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
This commit is contained in:
Zachary Wasserman 2016-09-28 21:21:39 -07:00 committed by Mike Arpaia
parent 6fb96d98f7
commit ba528a46f1
15 changed files with 753 additions and 155 deletions

View file

@ -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

View file

@ -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"}

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}
})
}
}

View file

@ -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
}
}

View file

@ -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")
}

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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":

View file

@ -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
}

View file

@ -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),
)
}