From ba528a46f1bf0a3f8800e5a31feeaf5ba591755d Mon Sep 17 00:00:00 2001 From: Zachary Wasserman Date: Wed, 28 Sep 2016 21:21:39 -0700 Subject: [PATCH] Build endpoints for osquery service methods (#245) - Establish a pattern for host authentication - Establish a pattern for error JSON - Add transport and make endpoint functions - Fix discovered bugs + update tests --- server/datastore/datastore_test.go | 3 + server/datastore/gorm.go | 4 +- server/errors/errors.go | 3 +- server/errors/errors_test.go | 4 +- server/kolide/osquery.go | 8 +- server/service/endpoint_middleware.go | 54 +++- server/service/endpoint_middleware_test.go | 114 ++++++++- server/service/endpoint_osquery.go | 139 ++++++++++ server/service/handler.go | 283 ++++++++++++--------- server/service/handler_test.go | 22 +- server/service/service_osquery.go | 73 +++++- server/service/service_osquery_test.go | 8 +- server/service/transport_error.go | 23 ++ server/service/transport_osquery.go | 53 ++++ server/service/transport_osquery_test.go | 117 +++++++++ 15 files changed, 753 insertions(+), 155 deletions(-) create mode 100644 server/service/endpoint_osquery.go create mode 100644 server/service/transport_osquery.go create mode 100644 server/service/transport_osquery_test.go diff --git a/server/datastore/datastore_test.go b/server/datastore/datastore_test.go index c437f72b08..9b54eeb4ff 100644 --- a/server/datastore/datastore_test.go +++ b/server/datastore/datastore_test.go @@ -130,6 +130,9 @@ func testAuthenticateHost(t *testing.T, db kolide.HostStore) { _, err := db.AuthenticateHost("7B1A9DC9-B042-489F-8D5A-EEC2412C95AA") assert.NotNil(t, err) + + _, err = db.AuthenticateHost("") + assert.NotNil(t, err) } // TestUser tests the UserStore interface diff --git a/server/datastore/gorm.go b/server/datastore/gorm.go index fe2188d31e..94ebac69e3 100644 --- a/server/datastore/gorm.go +++ b/server/datastore/gorm.go @@ -162,14 +162,14 @@ func (orm gormDB) EnrollHost(uuid, hostname, ip, platform string, nodeKeySize in func (orm gormDB) AuthenticateHost(nodeKey string) (*kolide.Host, error) { host := kolide.Host{NodeKey: nodeKey} - err := orm.DB.Where(&host).First(&host).Error + err := orm.DB.Where("node_key = ?", host.NodeKey).First(&host).Error if err != nil { switch err { case gorm.ErrRecordNotFound: e := errors.NewFromError( err, http.StatusUnauthorized, - "Unauthorized", + "invalid node key", ) // osqueryd expects the literal string "true" here e.Extra = map[string]interface{}{"node_invalid": "true"} diff --git a/server/errors/errors.go b/server/errors/errors.go index 414313621d..92f3394e92 100644 --- a/server/errors/errors.go +++ b/server/errors/errors.go @@ -1,7 +1,6 @@ package errors import ( - "fmt" "net/http" "github.com/Sirupsen/logrus" @@ -26,7 +25,7 @@ type KolideError struct { // Implementation of error interface func (e *KolideError) Error() string { - return fmt.Sprintf("Public: %s Private: %s Err: %+v", e.PublicMessage, e.PrivateMessage, e.Err) + return e.PublicMessage } // Create a new KolideError specifying the public and private messages. The diff --git a/server/errors/errors_test.go b/server/errors/errors_test.go index c77359afcf..23ad1d1893 100644 --- a/server/errors/errors_test.go +++ b/server/errors/errors_test.go @@ -42,9 +42,7 @@ func TestNewFromError(t *testing.T) { err := errors.New("Foo error") kolideErr := NewFromError(err, StatusUnprocessableEntity, "Public error") - assert.Equal(t, - "Public: Public error Private: Foo error Err: Foo error", - kolideErr.Error()) + assert.Equal(t, "Public error", kolideErr.Error()) expect := &KolideError{ Err: err, diff --git a/server/kolide/osquery.go b/server/kolide/osquery.go index 412981b437..e91ff4d3ef 100644 --- a/server/kolide/osquery.go +++ b/server/kolide/osquery.go @@ -30,11 +30,13 @@ type OsqueryStore interface { type OsqueryService interface { EnrollAgent(ctx context.Context, enrollSecret, hostIdentifier string) (string, error) - GetClientConfig(ctx context.Context, action string, data json.RawMessage) (*OsqueryConfig, error) + AuthenticateHost(ctx context.Context, nodeKey string) (*Host, error) + GetClientConfig(ctx context.Context) (*OsqueryConfig, error) GetDistributedQueries(ctx context.Context) (map[string]string, error) SubmitDistributedQueryResults(ctx context.Context, results OsqueryDistributedQueryResults) error - SubmitStatusLogs(ctx context.Context, logs []OsqueryResultLog) error - SubmitResultsLogs(ctx context.Context, logs []OsqueryStatusLog) error + SubmitStatusLogs(ctx context.Context, logs []OsqueryStatusLog) error + SubmitResultLogs(ctx context.Context, logs []OsqueryResultLog) error + SubmitLogs(ctx context.Context, logType string, data *json.RawMessage) error } type OsqueryDistributedQueryResults map[string][]map[string]string diff --git a/server/service/endpoint_middleware.go b/server/service/endpoint_middleware.go index 529979ccee..d5c602ffd4 100644 --- a/server/service/endpoint_middleware.go +++ b/server/service/endpoint_middleware.go @@ -3,9 +3,11 @@ package service import ( "errors" "fmt" + "reflect" jwt "github.com/dgrijalva/jwt-go" "github.com/go-kit/kit/endpoint" + hostctx "github.com/kolide/kolide-ose/server/contexts/host" "github.com/kolide/kolide-ose/server/contexts/token" "github.com/kolide/kolide-ose/server/contexts/viewer" "github.com/kolide/kolide-ose/server/kolide" @@ -14,21 +16,69 @@ import ( var errNoContext = errors.New("context key not set") -func authenticated(jwtKey string, svc kolide.Service, next endpoint.Endpoint) endpoint.Endpoint { +// authenticatedHost wraps an endpoint, checks the validity of the node_key +// provided in the request, and attaches the corresponding osquery host to the +// context for the request +func authenticatedHost(svc kolide.Service, next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { - // first check if already succesfuly set + nodeKey, err := getNodeKey(request) + if err != nil { + return nil, err + } + + host, err := svc.AuthenticateHost(ctx, nodeKey) + if err != nil { + return nil, err + } + + ctx = hostctx.NewContext(ctx, *host) + return next(ctx, request) + } +} + +func getNodeKey(r interface{}) (string, error) { + // Retrieve node key by reflection (note that our options here + // are limited by the fact that request is an interface{}) + v := reflect.ValueOf(r) + if v.Kind() != reflect.Struct { + return "", osqueryError{ + message: "request type is not struct. This is likely a Kolide programmer error.", + } + } + nodeKeyField := v.FieldByName("NodeKey") + if !nodeKeyField.IsValid() { + return "", osqueryError{ + message: "request struct missing NodeKey. This is likely a Kolide programmer error.", + } + } + if nodeKeyField.Kind() != reflect.String { + return "", osqueryError{ + message: "NodeKey is not a string. This is likely a Kolide programmer error.", + } + } + return nodeKeyField.String(), nil +} + +// authenticatedUser wraps an endpoint, requires that the Kolide user is +// authenticated, and populates the context with a Viewer struct for that user. +func authenticatedUser(jwtKey string, svc kolide.Service, next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + // first check if already successfully set if _, ok := viewer.FromContext(ctx); ok { return next(ctx, request) } + // if not succesful, try again this time with errors bearer, ok := token.FromContext(ctx) if !ok { return nil, authError{reason: "no auth token"} } + v, err := authViewer(ctx, jwtKey, bearer, svc) if err != nil { return nil, err } + ctx = viewer.NewContext(ctx, *v) return next(ctx, request) } diff --git a/server/service/endpoint_middleware_test.go b/server/service/endpoint_middleware_test.go index 64f5fde262..1fa432d4d7 100644 --- a/server/service/endpoint_middleware_test.go +++ b/server/service/endpoint_middleware_test.go @@ -1,7 +1,6 @@ package service import ( - "context" "testing" "github.com/go-kit/kit/endpoint" @@ -9,6 +8,8 @@ import ( "github.com/kolide/kolide-ose/server/datastore" "github.com/kolide/kolide-ose/server/kolide" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" ) // TestEndpointPermissions tests that @@ -120,3 +121,114 @@ func TestEndpointPermissions(t *testing.T) { }) } } + +// TestGetNodeKey tests the reflection logic for pulling the node key from +// various (fake) request types +func TestGetNodeKey(t *testing.T) { + type Foo struct { + Foo string + NodeKey string + } + + type Bar struct { + Bar string + NodeKey string + } + + type Nope struct { + Nope string + } + + type Almost struct { + NodeKey int + } + + var getNodeKeyTests = []struct { + i interface{} + expectKey string + shouldErr bool + }{ + { + i: Foo{Foo: "foo", NodeKey: "fookey"}, + expectKey: "fookey", + shouldErr: false, + }, + { + i: Bar{Bar: "bar", NodeKey: "barkey"}, + expectKey: "barkey", + shouldErr: false, + }, + { + i: Nope{Nope: "nope"}, + expectKey: "", + shouldErr: true, + }, + { + i: Almost{NodeKey: 10}, + expectKey: "", + shouldErr: true, + }, + } + + for _, tt := range getNodeKeyTests { + t.Run("", func(t *testing.T) { + key, err := getNodeKey(tt.i) + assert.Equal(t, tt.expectKey, key) + if tt.shouldErr { + assert.IsType(t, osqueryError{}, err) + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestAuthenticatedHost(t *testing.T) { + ds, err := datastore.New("gorm-sqlite3", "") + require.Nil(t, err) + svc, err := newTestService(ds) + require.Nil(t, err) + + endpoint := authenticatedHost( + svc, + func(ctx context.Context, request interface{}) (interface{}, error) { + return nil, nil + }, + ) + + ctx := context.Background() + goodNodeKey, err := svc.EnrollAgent(ctx, "", "host123") + assert.Nil(t, err) + require.NotEmpty(t, goodNodeKey) + + var authenticatedHostTests = []struct { + nodeKey string + shouldErr bool + }{ + { + nodeKey: "invalid", + shouldErr: true, + }, + { + nodeKey: "", + shouldErr: true, + }, + { + nodeKey: goodNodeKey, + shouldErr: false, + }, + } + + for _, tt := range authenticatedHostTests { + t.Run("", func(t *testing.T) { + var r = struct{ NodeKey string }{NodeKey: tt.nodeKey} + _, err = endpoint(context.Background(), r) + if tt.shouldErr { + assert.IsType(t, osqueryError{}, err) + } else { + assert.Nil(t, err) + } + }) + } + +} diff --git a/server/service/endpoint_osquery.go b/server/service/endpoint_osquery.go new file mode 100644 index 0000000000..d9a0fdeacb --- /dev/null +++ b/server/service/endpoint_osquery.go @@ -0,0 +1,139 @@ +package service + +import ( + "encoding/json" + + "github.com/go-kit/kit/endpoint" + "github.com/kolide/kolide-ose/server/kolide" + "golang.org/x/net/context" +) + +//////////////////////////////////////////////////////////////////////////////// +// Enroll Agent +//////////////////////////////////////////////////////////////////////////////// + +type enrollAgentRequest struct { + EnrollSecret string `json:"enroll_secret"` + HostIdentifier string `json:"host_identifier"` +} + +type enrollAgentResponse struct { + NodeKey string `json:"node_key,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r enrollAgentResponse) error() error { return r.Err } + +func makeEnrollAgentEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(enrollAgentRequest) + nodeKey, err := svc.EnrollAgent(ctx, req.EnrollSecret, req.HostIdentifier) + if err != nil { + return enrollAgentResponse{Err: err}, nil + } + return enrollAgentResponse{NodeKey: nodeKey}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Client Config +//////////////////////////////////////////////////////////////////////////////// + +type getClientConfigRequest struct { + NodeKey string `json:"node_key"` +} + +type getClientConfigResponse struct { + Config kolide.OsqueryConfig `json:"config,omitempty"` + Err error `json:"error,omitempty"` +} + +func (r getClientConfigResponse) error() error { return r.Err } + +func makeGetClientConfigEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + config, err := svc.GetClientConfig(ctx) + if err != nil { + return getClientConfigResponse{Err: err}, nil + } + return getClientConfigResponse{Config: *config}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Get Distributed Queries +//////////////////////////////////////////////////////////////////////////////// + +type getDistributedQueriesRequest struct { + NodeKey string `json:"node_key"` +} + +type getDistributedQueriesResponse struct { + Queries map[string]string `json:"queries"` + Err error `json:"error,omitempty"` +} + +func (r getDistributedQueriesResponse) error() error { return r.Err } + +func makeGetDistributedQueriesEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + queries, err := svc.GetDistributedQueries(ctx) + if err != nil { + return getDistributedQueriesResponse{Err: err}, nil + } + return getDistributedQueriesResponse{Queries: queries}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Write Distributed Query Results +//////////////////////////////////////////////////////////////////////////////// + +type submitDistributedQueryResultsRequest struct { + NodeKey string `json:"node_key"` + Results kolide.OsqueryDistributedQueryResults `json:"queries"` +} + +type submitDistributedQueryResultsResponse struct { + Err error `json:"error,omitempty"` +} + +func (r submitDistributedQueryResultsResponse) error() error { return r.Err } + +func makeSubmitDistributedQueryResultsEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(submitDistributedQueryResultsRequest) + err := svc.SubmitDistributedQueryResults(ctx, req.Results) + if err != nil { + return submitDistributedQueryResultsResponse{Err: err}, nil + } + return submitDistributedQueryResultsResponse{}, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Submit Logs +//////////////////////////////////////////////////////////////////////////////// + +type submitLogsRequest struct { + NodeKey string `json:"node_key"` + LogType string `json:"log_type"` + Data *json.RawMessage `json:"data"` +} + +type submitLogsResponse struct { + Err error `json:"error,omitempty"` +} + +func (r submitLogsResponse) error() error { return r.Err } + +func makeSubmitLogsEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(submitLogsRequest) + err := svc.SubmitLogs(ctx, req.LogType, req.Data) + if err != nil { + return submitLogsResponse{Err: err}, nil + } + return submitLogsResponse{}, nil + } +} diff --git a/server/service/handler.go b/server/service/handler.go index d221c9cb38..fa910eda97 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -13,108 +13,127 @@ import ( // KolideEndpoints is a collection of RPC endpoints implemented by the Kolide API. type KolideEndpoints struct { - Login endpoint.Endpoint - Logout endpoint.Endpoint - ForgotPassword endpoint.Endpoint - ResetPassword endpoint.Endpoint - Me endpoint.Endpoint - CreateUser endpoint.Endpoint - GetUser endpoint.Endpoint - ListUsers endpoint.Endpoint - ModifyUser endpoint.Endpoint - GetSessionsForUserInfo endpoint.Endpoint - DeleteSessionsForUser endpoint.Endpoint - GetSessionInfo endpoint.Endpoint - DeleteSession endpoint.Endpoint - GetAppConfig endpoint.Endpoint - ModifyAppConfig endpoint.Endpoint - CreateInvite endpoint.Endpoint - ListInvites endpoint.Endpoint - DeleteInvite endpoint.Endpoint - GetQuery endpoint.Endpoint - GetAllQueries endpoint.Endpoint - CreateQuery endpoint.Endpoint - ModifyQuery endpoint.Endpoint - DeleteQuery endpoint.Endpoint - GetPack endpoint.Endpoint - GetAllPacks endpoint.Endpoint - CreatePack endpoint.Endpoint - ModifyPack endpoint.Endpoint - DeletePack endpoint.Endpoint - AddQueryToPack endpoint.Endpoint - GetQueriesInPack endpoint.Endpoint - DeleteQueryFromPack endpoint.Endpoint + Login endpoint.Endpoint + Logout endpoint.Endpoint + ForgotPassword endpoint.Endpoint + ResetPassword endpoint.Endpoint + Me endpoint.Endpoint + CreateUser endpoint.Endpoint + GetUser endpoint.Endpoint + ListUsers endpoint.Endpoint + ModifyUser endpoint.Endpoint + GetSessionsForUserInfo endpoint.Endpoint + DeleteSessionsForUser endpoint.Endpoint + GetSessionInfo endpoint.Endpoint + DeleteSession endpoint.Endpoint + GetAppConfig endpoint.Endpoint + ModifyAppConfig endpoint.Endpoint + CreateInvite endpoint.Endpoint + ListInvites endpoint.Endpoint + DeleteInvite endpoint.Endpoint + GetQuery endpoint.Endpoint + GetAllQueries endpoint.Endpoint + CreateQuery endpoint.Endpoint + ModifyQuery endpoint.Endpoint + DeleteQuery endpoint.Endpoint + GetPack endpoint.Endpoint + GetAllPacks endpoint.Endpoint + CreatePack endpoint.Endpoint + ModifyPack endpoint.Endpoint + DeletePack endpoint.Endpoint + AddQueryToPack endpoint.Endpoint + GetQueriesInPack endpoint.Endpoint + DeleteQueryFromPack endpoint.Endpoint + EnrollAgent endpoint.Endpoint + GetClientConfig endpoint.Endpoint + GetDistributedQueries endpoint.Endpoint + SubmitDistributedQueryResults endpoint.Endpoint + SubmitLogs endpoint.Endpoint } // MakeKolideServerEndpoints creates the Kolide API endpoints. func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoints { return KolideEndpoints{ - Login: makeLoginEndpoint(svc), - Logout: makeLogoutEndpoint(svc), - ForgotPassword: makeForgotPasswordEndpoint(svc), - ResetPassword: makeResetPasswordEndpoint(svc), - CreateUser: makeCreateUserEndpoint(svc), - Me: authenticated(jwtKey, svc, makeGetSessionUserEndpoint(svc)), - GetUser: authenticated(jwtKey, svc, canReadUser(makeGetUserEndpoint(svc))), - ListUsers: authenticated(jwtKey, svc, canPerformActions(makeListUsersEndpoint(svc))), - ModifyUser: authenticated(jwtKey, svc, validateModifyUserRequest(makeModifyUserEndpoint(svc))), - GetSessionsForUserInfo: authenticated(jwtKey, svc, canReadUser(makeGetInfoAboutSessionsForUserEndpoint(svc))), - DeleteSessionsForUser: authenticated(jwtKey, svc, canModifyUser(makeDeleteSessionsForUserEndpoint(svc))), - GetSessionInfo: authenticated(jwtKey, svc, mustBeAdmin(makeGetInfoAboutSessionEndpoint(svc))), - DeleteSession: authenticated(jwtKey, svc, mustBeAdmin(makeDeleteSessionEndpoint(svc))), - GetAppConfig: authenticated(jwtKey, svc, makeGetAppConfigEndpoint(svc)), - ModifyAppConfig: authenticated(jwtKey, svc, mustBeAdmin(makeModifyAppConfigRequest(svc))), - CreateInvite: authenticated(jwtKey, svc, mustBeAdmin(makeCreateInviteEndpoint(svc))), - ListInvites: authenticated(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))), - DeleteInvite: authenticated(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))), - GetQuery: authenticated(jwtKey, svc, makeGetQueryEndpoint(svc)), - GetAllQueries: authenticated(jwtKey, svc, makeGetAllQueriesEndpoint(svc)), - CreateQuery: authenticated(jwtKey, svc, makeCreateQueryEndpoint(svc)), - ModifyQuery: authenticated(jwtKey, svc, makeModifyQueryEndpoint(svc)), - DeleteQuery: authenticated(jwtKey, svc, makeDeleteQueryEndpoint(svc)), - GetPack: authenticated(jwtKey, svc, makeGetPackEndpoint(svc)), - GetAllPacks: authenticated(jwtKey, svc, makeGetAllPacksEndpoint(svc)), - CreatePack: authenticated(jwtKey, svc, makeCreatePackEndpoint(svc)), - ModifyPack: authenticated(jwtKey, svc, makeModifyPackEndpoint(svc)), - DeletePack: authenticated(jwtKey, svc, makeDeletePackEndpoint(svc)), - AddQueryToPack: authenticated(jwtKey, svc, makeAddQueryToPackEndpoint(svc)), - GetQueriesInPack: authenticated(jwtKey, svc, makeGetQueriesInPackEndpoint(svc)), - DeleteQueryFromPack: authenticated(jwtKey, svc, makeDeleteQueryFromPackEndpoint(svc)), + Login: makeLoginEndpoint(svc), + Logout: makeLogoutEndpoint(svc), + ForgotPassword: makeForgotPasswordEndpoint(svc), + ResetPassword: makeResetPasswordEndpoint(svc), + CreateUser: makeCreateUserEndpoint(svc), + + // Authenticated user endpoints + Me: authenticatedUser(jwtKey, svc, makeGetSessionUserEndpoint(svc)), + GetUser: authenticatedUser(jwtKey, svc, canReadUser(makeGetUserEndpoint(svc))), + ListUsers: authenticatedUser(jwtKey, svc, canPerformActions(makeListUsersEndpoint(svc))), + ModifyUser: authenticatedUser(jwtKey, svc, validateModifyUserRequest(makeModifyUserEndpoint(svc))), + GetSessionsForUserInfo: authenticatedUser(jwtKey, svc, canReadUser(makeGetInfoAboutSessionsForUserEndpoint(svc))), + DeleteSessionsForUser: authenticatedUser(jwtKey, svc, canModifyUser(makeDeleteSessionsForUserEndpoint(svc))), + GetSessionInfo: authenticatedUser(jwtKey, svc, mustBeAdmin(makeGetInfoAboutSessionEndpoint(svc))), + DeleteSession: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteSessionEndpoint(svc))), + GetAppConfig: authenticatedUser(jwtKey, svc, makeGetAppConfigEndpoint(svc)), + ModifyAppConfig: authenticatedUser(jwtKey, svc, mustBeAdmin(makeModifyAppConfigRequest(svc))), + CreateInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeCreateInviteEndpoint(svc))), + ListInvites: authenticatedUser(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))), + DeleteInvite: authenticatedUser(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))), + GetQuery: authenticatedUser(jwtKey, svc, makeGetQueryEndpoint(svc)), + GetAllQueries: authenticatedUser(jwtKey, svc, makeGetAllQueriesEndpoint(svc)), + CreateQuery: authenticatedUser(jwtKey, svc, makeCreateQueryEndpoint(svc)), + ModifyQuery: authenticatedUser(jwtKey, svc, makeModifyQueryEndpoint(svc)), + DeleteQuery: authenticatedUser(jwtKey, svc, makeDeleteQueryEndpoint(svc)), + GetPack: authenticatedUser(jwtKey, svc, makeGetPackEndpoint(svc)), + GetAllPacks: authenticatedUser(jwtKey, svc, makeGetAllPacksEndpoint(svc)), + CreatePack: authenticatedUser(jwtKey, svc, makeCreatePackEndpoint(svc)), + ModifyPack: authenticatedUser(jwtKey, svc, makeModifyPackEndpoint(svc)), + DeletePack: authenticatedUser(jwtKey, svc, makeDeletePackEndpoint(svc)), + AddQueryToPack: authenticatedUser(jwtKey, svc, makeAddQueryToPackEndpoint(svc)), + GetQueriesInPack: authenticatedUser(jwtKey, svc, makeGetQueriesInPackEndpoint(svc)), + DeleteQueryFromPack: authenticatedUser(jwtKey, svc, makeDeleteQueryFromPackEndpoint(svc)), + + // Osquery endpoints + EnrollAgent: makeEnrollAgentEndpoint(svc), + GetClientConfig: authenticatedHost(svc, makeGetClientConfigEndpoint(svc)), + GetDistributedQueries: authenticatedHost(svc, makeGetDistributedQueriesEndpoint(svc)), + SubmitDistributedQueryResults: authenticatedHost(svc, makeSubmitDistributedQueryResultsEndpoint(svc)), + SubmitLogs: authenticatedHost(svc, makeSubmitLogsEndpoint(svc)), } } type kolideHandlers struct { - Login *kithttp.Server - Logout *kithttp.Server - ForgotPassword *kithttp.Server - ResetPassword *kithttp.Server - Me *kithttp.Server - CreateUser *kithttp.Server - GetUser *kithttp.Server - ListUsers *kithttp.Server - ModifyUser *kithttp.Server - GetSessionsForUserInfo *kithttp.Server - DeleteSessionsForUser *kithttp.Server - GetSessionInfo *kithttp.Server - DeleteSession *kithttp.Server - GetAppConfig *kithttp.Server - ModifyAppConfig *kithttp.Server - CreateInvite *kithttp.Server - ListInvites *kithttp.Server - DeleteInvite *kithttp.Server - GetQuery *kithttp.Server - GetAllQueries *kithttp.Server - CreateQuery *kithttp.Server - ModifyQuery *kithttp.Server - DeleteQuery *kithttp.Server - GetPack *kithttp.Server - GetAllPacks *kithttp.Server - CreatePack *kithttp.Server - ModifyPack *kithttp.Server - DeletePack *kithttp.Server - AddQueryToPack *kithttp.Server - GetQueriesInPack *kithttp.Server - DeleteQueryFromPack *kithttp.Server + Login *kithttp.Server + Logout *kithttp.Server + ForgotPassword *kithttp.Server + ResetPassword *kithttp.Server + Me *kithttp.Server + CreateUser *kithttp.Server + GetUser *kithttp.Server + ListUsers *kithttp.Server + ModifyUser *kithttp.Server + GetSessionsForUserInfo *kithttp.Server + DeleteSessionsForUser *kithttp.Server + GetSessionInfo *kithttp.Server + DeleteSession *kithttp.Server + GetAppConfig *kithttp.Server + ModifyAppConfig *kithttp.Server + CreateInvite *kithttp.Server + ListInvites *kithttp.Server + DeleteInvite *kithttp.Server + GetQuery *kithttp.Server + GetAllQueries *kithttp.Server + CreateQuery *kithttp.Server + ModifyQuery *kithttp.Server + DeleteQuery *kithttp.Server + GetPack *kithttp.Server + GetAllPacks *kithttp.Server + CreatePack *kithttp.Server + ModifyPack *kithttp.Server + DeletePack *kithttp.Server + AddQueryToPack *kithttp.Server + GetQueriesInPack *kithttp.Server + DeleteQueryFromPack *kithttp.Server + EnrollAgent *kithttp.Server + GetClientConfig *kithttp.Server + GetDistributedQueries *kithttp.Server + SubmitDistributedQueryResults *kithttp.Server + SubmitLogs *kithttp.Server } func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithttp.ServerOption) kolideHandlers { @@ -122,37 +141,42 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt return kithttp.NewServer(ctx, e, decodeFn, encodeResponse, opts...) } return kolideHandlers{ - Login: newServer(e.Login, decodeLoginRequest), - Logout: newServer(e.Logout, decodeNoParamsRequest), - ForgotPassword: newServer(e.ForgotPassword, decodeForgotPasswordRequest), - ResetPassword: newServer(e.ResetPassword, decodeResetPasswordRequest), - Me: newServer(e.Me, decodeNoParamsRequest), - CreateUser: newServer(e.CreateUser, decodeCreateUserRequest), - GetUser: newServer(e.GetUser, decodeGetUserRequest), - ListUsers: newServer(e.ListUsers, decodeNoParamsRequest), - ModifyUser: newServer(e.ModifyUser, decodeModifyUserRequest), - GetSessionsForUserInfo: newServer(e.GetSessionsForUserInfo, decodeGetInfoAboutSessionsForUserRequest), - DeleteSessionsForUser: newServer(e.DeleteSessionsForUser, decodeDeleteSessionsForUserRequest), - GetSessionInfo: newServer(e.GetSessionInfo, decodeGetInfoAboutSessionRequest), - DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest), - GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest), - ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest), - CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), - ListInvites: newServer(e.ListInvites, decodeNoParamsRequest), - DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), - GetQuery: newServer(e.GetQuery, decodeGetQueryRequest), - GetAllQueries: newServer(e.GetAllQueries, decodeGetQueryRequest), - CreateQuery: newServer(e.CreateQuery, decodeCreateQueryRequest), - ModifyQuery: newServer(e.ModifyQuery, decodeModifyQueryRequest), - DeleteQuery: newServer(e.DeleteQuery, decodeDeleteQueryRequest), - GetPack: newServer(e.GetPack, decodeGetPackRequest), - GetAllPacks: newServer(e.GetAllPacks, decodeNoParamsRequest), - CreatePack: newServer(e.CreatePack, decodeCreatePackRequest), - ModifyPack: newServer(e.ModifyPack, decodeModifyPackRequest), - DeletePack: newServer(e.DeletePack, decodeDeletePackRequest), - AddQueryToPack: newServer(e.AddQueryToPack, decodeAddQueryToPackRequest), - GetQueriesInPack: newServer(e.GetQueriesInPack, decodeGetQueriesInPackRequest), - DeleteQueryFromPack: newServer(e.DeleteQueryFromPack, decodeDeleteQueryFromPackRequest), + Login: newServer(e.Login, decodeLoginRequest), + Logout: newServer(e.Logout, decodeNoParamsRequest), + ForgotPassword: newServer(e.ForgotPassword, decodeForgotPasswordRequest), + ResetPassword: newServer(e.ResetPassword, decodeResetPasswordRequest), + Me: newServer(e.Me, decodeNoParamsRequest), + CreateUser: newServer(e.CreateUser, decodeCreateUserRequest), + GetUser: newServer(e.GetUser, decodeGetUserRequest), + ListUsers: newServer(e.ListUsers, decodeNoParamsRequest), + ModifyUser: newServer(e.ModifyUser, decodeModifyUserRequest), + GetSessionsForUserInfo: newServer(e.GetSessionsForUserInfo, decodeGetInfoAboutSessionsForUserRequest), + DeleteSessionsForUser: newServer(e.DeleteSessionsForUser, decodeDeleteSessionsForUserRequest), + GetSessionInfo: newServer(e.GetSessionInfo, decodeGetInfoAboutSessionRequest), + DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest), + GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest), + ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest), + CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), + ListInvites: newServer(e.ListInvites, decodeNoParamsRequest), + DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), + GetQuery: newServer(e.GetQuery, decodeGetQueryRequest), + GetAllQueries: newServer(e.GetAllQueries, decodeGetQueryRequest), + CreateQuery: newServer(e.CreateQuery, decodeCreateQueryRequest), + ModifyQuery: newServer(e.ModifyQuery, decodeModifyQueryRequest), + DeleteQuery: newServer(e.DeleteQuery, decodeDeleteQueryRequest), + GetPack: newServer(e.GetPack, decodeGetPackRequest), + GetAllPacks: newServer(e.GetAllPacks, decodeNoParamsRequest), + CreatePack: newServer(e.CreatePack, decodeCreatePackRequest), + ModifyPack: newServer(e.ModifyPack, decodeModifyPackRequest), + DeletePack: newServer(e.DeletePack, decodeDeletePackRequest), + AddQueryToPack: newServer(e.AddQueryToPack, decodeAddQueryToPackRequest), + GetQueriesInPack: newServer(e.GetQueriesInPack, decodeGetQueriesInPackRequest), + DeleteQueryFromPack: newServer(e.DeleteQueryFromPack, decodeDeleteQueryFromPackRequest), + EnrollAgent: newServer(e.EnrollAgent, decodeEnrollAgentRequest), + GetClientConfig: newServer(e.GetClientConfig, decodeGetClientConfigRequest), + GetDistributedQueries: newServer(e.GetDistributedQueries, decodeGetDistributedQueriesRequest), + SubmitDistributedQueryResults: newServer(e.SubmitDistributedQueryResults, decodeSubmitDistributedQueryResultsRequest), + SubmitLogs: newServer(e.SubmitLogs, decodeSubmitLogsRequest), } } @@ -184,24 +208,29 @@ func attachKolideAPIRoutes(r *mux.Router, h kolideHandlers) { r.Handle("/api/v1/kolide/forgot_password", h.ForgotPassword).Methods("POST") r.Handle("/api/v1/kolide/reset_password", h.ResetPassword).Methods("POST") r.Handle("/api/v1/kolide/me", h.Me).Methods("GET") + r.Handle("/api/v1/kolide/users", h.ListUsers).Methods("GET") r.Handle("/api/v1/kolide/users", h.CreateUser).Methods("POST") r.Handle("/api/v1/kolide/users/{id}", h.GetUser).Methods("GET") r.Handle("/api/v1/kolide/users/{id}", h.ModifyUser).Methods("PATCH") r.Handle("/api/v1/kolide/users/{id}/sessions", h.GetSessionsForUserInfo).Methods("GET") r.Handle("/api/v1/kolide/users/{id}/sessions", h.DeleteSessionsForUser).Methods("DELETE") + r.Handle("/api/v1/kolide/sessions/{id}", h.GetSessionInfo).Methods("GET") r.Handle("/api/v1/kolide/sessions/{id}", h.DeleteSession).Methods("DELETE") + r.Handle("/api/v1/kolide/config", h.GetAppConfig).Methods("GET") r.Handle("/api/v1/kolide/config", h.ModifyAppConfig).Methods("PATCH") r.Handle("/api/v1/kolide/invites", h.CreateInvite).Methods("POST") r.Handle("/api/v1/kolide/invites", h.ListInvites).Methods("GET") r.Handle("/api/v1/kolide/invites/{id}", h.DeleteInvite).Methods("DELETE") + r.Handle("/api/v1/kolide/queries/{id}", h.GetQuery).Methods("GET") r.Handle("/api/v1/kolide/queries", h.GetAllQueries).Methods("GET") r.Handle("/api/v1/kolide/queries", h.CreateQuery).Methods("POST") r.Handle("/api/v1/kolide/queries/{id}", h.ModifyQuery).Methods("PATCH") r.Handle("/api/v1/kolide/queries/{id}", h.DeleteQuery).Methods("DELETE") + r.Handle("/api/v1/kolide/packs/{id}", h.GetPack).Methods("GET") r.Handle("/api/v1/kolide/packs", h.GetAllPacks).Methods("GET") r.Handle("/api/v1/kolide/packs", h.CreatePack).Methods("POST") @@ -210,4 +239,10 @@ func attachKolideAPIRoutes(r *mux.Router, h kolideHandlers) { r.Handle("/api/v1/kolide/packs/{pid}/queries/{qid}", h.AddQueryToPack).Methods("POST") r.Handle("/api/v1/kolide/packs/{id}/queries", h.GetQueriesInPack).Methods("GET") r.Handle("/api/v1/kolide/packs/{pid}/queries/{qid}", h.DeleteQueryFromPack).Methods("DELETE") + + r.Handle("/api/v1/osquery/enroll", h.EnrollAgent).Methods("POST") + r.Handle("/api/v1/osquery/config", h.GetClientConfig).Methods("POST") + r.Handle("/api/v1/osquery/distributed/read", h.GetDistributedQueries).Methods("POST") + r.Handle("/api/v1/osquery/distributed/write", h.SubmitDistributedQueryResults).Methods("POST") + r.Handle("/api/v1/osquery/log", h.SubmitLogs).Methods("POST") } diff --git a/server/service/handler_test.go b/server/service/handler_test.go index 45cc7a266f..30c73ef02c 100644 --- a/server/service/handler_test.go +++ b/server/service/handler_test.go @@ -25,7 +25,7 @@ func TestAPIRoutes(t *testing.T) { kh := makeKolideKitHandlers(ctx, ke, nil) attachKolideAPIRoutes(r, kh) handler := mux.NewRouter() - handler.PathPrefix("/api/v1/kolide").Handler(r) + handler.PathPrefix("/").Handler(r) var routes = []struct { verb string @@ -135,6 +135,26 @@ func TestAPIRoutes(t *testing.T) { verb: "DELETE", uri: "/api/v1/kolide/packs/1/queries/2", }, + { + verb: "POST", + uri: "/api/v1/osquery/enroll", + }, + { + verb: "POST", + uri: "/api/v1/osquery/config", + }, + { + verb: "POST", + uri: "/api/v1/osquery/distributed/read", + }, + { + verb: "POST", + uri: "/api/v1/osquery/distributed/write", + }, + { + verb: "POST", + uri: "/api/v1/osquery/log", + }, } for _, route := range routes { diff --git a/server/service/service_osquery.go b/server/service/service_osquery.go index 005c1aac4e..fc1ba8b092 100644 --- a/server/service/service_osquery.go +++ b/server/service/service_osquery.go @@ -2,45 +2,63 @@ package service import ( "encoding/json" - "fmt" "net/http" + hostctx "github.com/kolide/kolide-ose/server/contexts/host" "github.com/kolide/kolide-ose/server/errors" "github.com/kolide/kolide-ose/server/kolide" - "github.com/kolide/kolide-ose/server/contexts/host" "golang.org/x/net/context" ) type osqueryError struct { - message string + message string + nodeInvalid bool } func (e osqueryError) Error() string { return e.message } +func (e osqueryError) NodeInvalid() bool { + return e.nodeInvalid +} + +func (svc service) AuthenticateHost(ctx context.Context, nodeKey string) (*kolide.Host, error) { + if nodeKey == "" { + return nil, osqueryError{ + message: "authentication error: missing node key", + nodeInvalid: true, + } + } + host, err := svc.ds.AuthenticateHost(nodeKey) + if err != nil { + return nil, osqueryError{ + message: "authentication error: " + err.Error(), + nodeInvalid: true, + } + } + return host, nil +} + func (svc service) EnrollAgent(ctx context.Context, enrollSecret, hostIdentifier string) (string, error) { if enrollSecret != svc.config.Osquery.EnrollSecret { - return "", errors.New( - "Node key invalid", - fmt.Sprintf("Invalid node key provided: %s", enrollSecret), - ) + return "", osqueryError{message: "invalid enroll secret", nodeInvalid: true} } host, err := svc.ds.EnrollHost(hostIdentifier, "", "", "", svc.config.Osquery.NodeKeySize) if err != nil { - return "", err + return "", osqueryError{message: "enrollment failed: " + err.Error(), nodeInvalid: true} } return host.NodeKey, nil } -func (svc service) GetClientConfig(ctx context.Context, action string, data json.RawMessage) (*kolide.OsqueryConfig, error) { +func (svc service) GetClientConfig(ctx context.Context) (*kolide.OsqueryConfig, error) { var config kolide.OsqueryConfig return &config, nil } -func (svc service) SubmitStatusLogs(ctx context.Context, logs []kolide.OsqueryResultLog) error { +func (svc service) SubmitStatusLogs(ctx context.Context, logs []kolide.OsqueryStatusLog) error { for _, log := range logs { err := json.NewEncoder(svc.osqueryStatusLogWriter).Encode(log) if err != nil { @@ -50,7 +68,7 @@ func (svc service) SubmitStatusLogs(ctx context.Context, logs []kolide.OsqueryRe return nil } -func (svc service) SubmitResultsLogs(ctx context.Context, logs []kolide.OsqueryStatusLog) error { +func (svc service) SubmitResultLogs(ctx context.Context, logs []kolide.OsqueryResultLog) error { for _, log := range logs { err := json.NewEncoder(svc.osqueryResultsLogWriter).Encode(log) if err != nil { @@ -60,6 +78,35 @@ func (svc service) SubmitResultsLogs(ctx context.Context, logs []kolide.OsqueryS return nil } +func (svc service) SubmitLogs(ctx context.Context, logType string, data *json.RawMessage) error { + host, ok := hostctx.FromContext(ctx) + if !ok { + return osqueryError{message: "internal error: missing host from request context"} + } + + var err error + switch logType { + case "status": + // TODO: Decode and submit logs + + case "result": + // TODO: Decode and submit logs + + default: + err = osqueryError{message: "unknown log type: " + logType} + svc.logger.Log("method", "SubmitLogs", "err", err) + } + + if err != nil { + return osqueryError{message: "log ingestion failed: " + err.Error()} + } + + // TODO: Update update_time of host + _ = host + + return nil +} + // hostLabelQueryPrefix is appended before the query name when a query is // provided as a label query. This allows the results to be retrieved when // osqueryd writes the distributed query results. @@ -82,9 +129,9 @@ func hostDetailQueries(host kolide.Host) map[string]string { func (svc service) GetDistributedQueries(ctx context.Context) (map[string]string, error) { queries := make(map[string]string) - host, ok := host.FromContext(ctx) + host, ok := hostctx.FromContext(ctx) if !ok { - return nil, errNoContext + return nil, osqueryError{message: "internal error: missing host from request context"} } queries = hostDetailQueries(host) diff --git a/server/service/service_osquery_test.go b/server/service/service_osquery_test.go index 8f2456773c..e7ecf4f2d9 100644 --- a/server/service/service_osquery_test.go +++ b/server/service/service_osquery_test.go @@ -7,9 +7,9 @@ import ( "time" "github.com/WatchBeam/clock" + hostctx "github.com/kolide/kolide-ose/server/contexts/host" "github.com/kolide/kolide-ose/server/datastore" "github.com/kolide/kolide-ose/server/kolide" - hostContext "github.com/kolide/kolide-ose/server/contexts/host" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -102,7 +102,7 @@ func TestGetDistributedQueries(t *testing.T) { require.Len(t, hosts, 1) host := hosts[0] - ctx = hostContext.NewContext(ctx, *host) + ctx = hostctx.NewContext(ctx, *host) // With no platform set, we should get the details query queries, err := svc.GetDistributedQueries(ctx) @@ -116,8 +116,8 @@ func TestGetDistributedQueries(t *testing.T) { } host.Platform = "darwin" - - ctx = hostContext.NewContext(ctx, *host) + ds.SaveHost(host) + ctx = hostctx.NewContext(ctx, *host) // With the platform set, we should get the label queries (but none // exist yet) diff --git a/server/service/transport_error.go b/server/service/transport_error.go index 580585879b..c7abed83ba 100644 --- a/server/service/transport_error.go +++ b/server/service/transport_error.go @@ -71,6 +71,29 @@ func encodeError(ctx context.Context, err error, w http.ResponseWriter) { return } + type osqueryError interface { + error + NodeInvalid() bool + } + if e, ok := err.(osqueryError); ok { + // osquery expects to receive the node_invalid key when a TLS + // request provides an invalid node_key for authentication. It + // doesn't use the error message provided, but we provide this + // for debugging purposes (and perhaps osquery will use this + // error message in the future). + + errMap := map[string]interface{}{"error": e.Error()} + if e.NodeInvalid() { + w.WriteHeader(http.StatusUnauthorized) + errMap["node_invalid"] = true + } else { + w.WriteHeader(http.StatusInternalServerError) + } + + enc.Encode(errMap) + return + } + // Other errors switch domain { case "service": diff --git a/server/service/transport_osquery.go b/server/service/transport_osquery.go new file mode 100644 index 0000000000..ed490d384a --- /dev/null +++ b/server/service/transport_osquery.go @@ -0,0 +1,53 @@ +package service + +import ( + "encoding/json" + "net/http" + + "golang.org/x/net/context" +) + +func decodeEnrollAgentRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req enrollAgentRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + + return req, nil +} + +func decodeGetClientConfigRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req getClientConfigRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + + return req, nil +} + +func decodeGetDistributedQueriesRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req getDistributedQueriesRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + + return req, nil +} + +func decodeSubmitDistributedQueryResultsRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req submitDistributedQueryResultsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + + return req, nil +} + +func decodeSubmitLogsRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req submitLogsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + + return req, nil +} diff --git a/server/service/transport_osquery_test.go b/server/service/transport_osquery_test.go new file mode 100644 index 0000000000..853ac78130 --- /dev/null +++ b/server/service/transport_osquery_test.go @@ -0,0 +1,117 @@ +package service + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/kolide/kolide-ose/server/kolide" + "github.com/stretchr/testify/assert" +) + +func TestDecodeEnrollAgentRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/osquery/enroll", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeEnrollAgentRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(enrollAgentRequest) + assert.Equal(t, "secret", params.EnrollSecret) + assert.Equal(t, "uuid", params.HostIdentifier) + }).Methods("POST") + + var body bytes.Buffer + body.Write([]byte(`{ + "enroll_secret": "secret", + "host_identifier": "uuid" + }`)) + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("POST", "/api/v1/osquery/enroll", &body), + ) +} + +func TestDecodeGetClientConfigRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/osquery/enroll", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeGetClientConfigRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(getClientConfigRequest) + assert.Equal(t, "key", params.NodeKey) + }).Methods("POST") + + var body bytes.Buffer + body.Write([]byte(`{ + "node_key": "key" + }`)) + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("POST", "/api/v1/osquery/enroll", &body), + ) +} + +func TestDecodeGetDistributedQueriesRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/osquery/enroll", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeGetDistributedQueriesRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(getDistributedQueriesRequest) + assert.Equal(t, "key", params.NodeKey) + }).Methods("POST") + + var body bytes.Buffer + body.Write([]byte(`{ + "node_key": "key" + }`)) + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("POST", "/api/v1/osquery/enroll", &body), + ) +} + +func TestDecodeSubmitDistributedQueryResultsRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/osquery/enroll", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeSubmitDistributedQueryResultsRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(submitDistributedQueryResultsRequest) + assert.Equal(t, "key", params.NodeKey) + assert.Equal(t, kolide.OsqueryDistributedQueryResults{ + "id1": { + {"col1": "val1", "col2": "val2"}, + {"col1": "val3", "col2": "val4"}, + }, + "id2": { + {"col3": "val5", "col4": "val6"}, + }, + }, params.Results) + }).Methods("POST") + + var body bytes.Buffer + body.Write([]byte(`{ + "node_key": "key", + "queries": { + "id1": [ + {"col1": "val1", "col2": "val2"}, + {"col1": "val3", "col2": "val4"} + ], + "id2": [ + {"col3": "val5", "col4": "val6"} + ] + } + }`)) + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("POST", "/api/v1/osquery/enroll", &body), + ) +}