diff --git a/cli/serve.go b/cli/serve.go index 0ac11f555d..f0d797ab75 100644 --- a/cli/serve.go +++ b/cli/serve.go @@ -25,7 +25,7 @@ import ( ) func createServeCmd(configManager config.Manager) *cobra.Command { - var devMode bool = false + var devMode = false serveCmd := &cobra.Command{ Use: "serve", @@ -117,7 +117,16 @@ the way that the kolide server works. httpLogger := kitlog.NewContext(logger).With("component", "http") - apiHandler := service.MakeHandler(ctx, svc, config.Auth.JwtKey, httpLogger) + var apiHandler http.Handler + { + apiHandler = service.MakeHandler(ctx, svc, config.Auth.JwtKey, httpLogger) + // WithSetup will check if first time setup is required + // By performing the same check inside main, we can make server startups + // more efficient after the first startup. + if service.RequireSetup(svc, logger) { + apiHandler = service.WithSetup(svc, logger, apiHandler) + } + } http.Handle("/api/", apiHandler) http.Handle("/version", version.Handler()) http.Handle("/metrics", prometheus.Handler()) diff --git a/server/kolide/app.go b/server/kolide/app.go index 3242f24f19..479ef090ac 100644 --- a/server/kolide/app.go +++ b/server/kolide/app.go @@ -42,7 +42,7 @@ type OrgInfo struct { // ServerSettings contains general settings about the kolide App. type ServerSettings struct { - KolideServerURL *string `json:"web_address_url,omitempty"` + KolideServerURL *string `json:"kolide_server_url,omitempty"` } type OrderDirection int diff --git a/server/kolide/users.go b/server/kolide/users.go index 6c97e4604b..5581d423e8 100644 --- a/server/kolide/users.go +++ b/server/kolide/users.go @@ -25,6 +25,10 @@ type UserService interface { // NewUser creates a new User from a request Payload NewUser(ctx context.Context, p UserPayload) (user *User, err error) + // NewAdminCreatedUser allows an admin to create a new user without + // first creating and validating invite tokens. + NewAdminCreatedUser(ctx context.Context, p UserPayload) (user *User, err error) + // User returns a valid User given a User ID User(ctx context.Context, id uint) (user *User, err error) diff --git a/server/service/endpoint_setup.go b/server/service/endpoint_setup.go new file mode 100644 index 0000000000..cb6ee25fb7 --- /dev/null +++ b/server/service/endpoint_setup.go @@ -0,0 +1,60 @@ +package service + +import ( + "golang.org/x/net/context" + + "github.com/go-kit/kit/endpoint" + "github.com/kolide/kolide-ose/server/kolide" +) + +type setupRequest struct { + Admin *kolide.UserPayload `json:"admin"` + OrgInfo *kolide.OrgInfo `json:"org_info"` + KolideServerURL *string `json:"kolide_server_url"` +} + +type setupResponse struct { + Admin *kolide.User `json:"admin,omitempty"` + OrgInfo *kolide.OrgInfo `json:"org_info,omitempty"` + KolideServerURL *string `json:"kolide_server_url"` + Err error `json:"error,omitempty"` +} + +func (r setupResponse) error() error { return r.Err } + +func makeSetupEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + var ( + admin *kolide.User + config *kolide.AppConfig + configPayload kolide.AppConfigPayload + err error + ) + req := request.(setupRequest) + if req.Admin != nil { + admin, err = svc.NewAdminCreatedUser(ctx, *req.Admin) + if err != nil { + return setupResponse{Err: err}, nil + } + } + + if req.OrgInfo != nil { + configPayload.OrgInfo = req.OrgInfo + } + if req.KolideServerURL != nil { + configPayload.ServerSettings = &kolide.ServerSettings{KolideServerURL: req.KolideServerURL} + } + config, err = svc.NewAppConfig(ctx, configPayload) + if err != nil { + return setupResponse{Err: err}, nil + } + return setupResponse{ + Admin: admin, + OrgInfo: &kolide.OrgInfo{ + OrgName: &config.OrgName, + OrgLogoURL: &config.OrgLogoURL, + }, + KolideServerURL: &config.KolideServerURL, + }, nil + } +} diff --git a/server/service/handler.go b/server/service/handler.go index 3fdd48a367..0be21f3bb3 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -1,6 +1,7 @@ package service import ( + "encoding/json" "net/http" "github.com/go-kit/kit/endpoint" @@ -303,3 +304,39 @@ func attachKolideAPIRoutes(r *mux.Router, h kolideHandlers) { r.Handle("/api/v1/osquery/distributed/write", h.SubmitDistributedQueryResults).Methods("POST") r.Handle("/api/v1/osquery/log", h.SubmitLogs).Methods("POST") } + +// WithSetup is an http middleware that checks if a database user exists. +// If one does, it serves the API with a setup middleware. +// If the server is already configured, the default API handler is exposed. +func WithSetup(svc kolide.Service, logger kitlog.Logger, next http.Handler) http.Handler { + configRouter := http.NewServeMux() + configRouter.Handle("/api/v1/kolide/config", http.HandlerFunc(forceSetup)) + configRouter.Handle("/api/v1/setup", kithttp.NewServer( + context.Background(), + makeSetupEndpoint(svc), + decodeSetupRequest, + encodeResponse, + )) + if RequireSetup(svc, logger) { + return configRouter + } + return next +} + +func forceSetup(w http.ResponseWriter, r *http.Request) { + response := map[string]bool{ + "require_setup": true, + } + if err := json.NewEncoder(w).Encode(&response); err != nil { + encodeError(context.Background(), err, w) + } +} + +// RequireSetup checks if the service must be configured by an administrator. +func RequireSetup(svc kolide.Service, logger kitlog.Logger) bool { + users, err := svc.ListUsers(context.Background(), kolide.ListOptions{Page: 1, PerPage: 1}) + if err != nil { + logger.Log("err", err) + } + return len(users) == 0 +} diff --git a/server/service/service_appconfig_test.go b/server/service/service_appconfig_test.go index 5720232211..9f1d511c71 100644 --- a/server/service/service_appconfig_test.go +++ b/server/service/service_appconfig_test.go @@ -10,16 +10,16 @@ import ( "golang.org/x/net/context" ) -func TestCreateOrgInfo(t *testing.T) { +func TestCreateAppConfig(t *testing.T) { ds, err := datastore.New("inmem", "") require.Nil(t, err) svc, err := newTestService(ds) require.Nil(t, err) - var orgInfoTests = []struct { - infoPayload kolide.AppConfigPayload + var appConfigTests = []struct { + configPayload kolide.AppConfigPayload }{ { - infoPayload: kolide.AppConfigPayload{ + configPayload: kolide.AppConfigPayload{ OrgInfo: &kolide.OrgInfo{ OrgLogoURL: stringPtr("acme.co/images/logo.png"), OrgName: stringPtr("Acme"), @@ -31,11 +31,11 @@ func TestCreateOrgInfo(t *testing.T) { }, } - for _, tt := range orgInfoTests { - result, err := svc.NewAppConfig(context.Background(), tt.infoPayload) + for _, tt := range appConfigTests { + result, err := svc.NewAppConfig(context.Background(), tt.configPayload) require.Nil(t, err) - payload := tt.infoPayload + payload := tt.configPayload assert.NotEmpty(t, result.ID) assert.Equal(t, *payload.OrgInfo.OrgLogoURL, result.OrgLogoURL) assert.Equal(t, *payload.OrgInfo.OrgName, result.OrgName) diff --git a/server/service/service_users.go b/server/service/service_users.go index 7dd8b8b04f..5c3bc27d07 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -20,6 +20,22 @@ func (svc service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.U if err != nil { return nil, err } + user, err := svc.newUser(p) + if err != nil { + return nil, err + } + err = svc.ds.DeleteInvite(invite) + if err != nil { + return nil, err + } + return user, nil +} + +func (svc service) NewAdminCreatedUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { + return svc.newUser(p) +} + +func (svc service) newUser(p kolide.UserPayload) (*kolide.User, error) { user, err := p.User(svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost) if err != nil { return nil, err @@ -28,10 +44,6 @@ func (svc service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.U if err != nil { return nil, err } - err = svc.ds.DeleteInvite(invite) - if err != nil { - return nil, err - } return user, nil } diff --git a/server/service/transport_setup.go b/server/service/transport_setup.go new file mode 100644 index 0000000000..a287136d21 --- /dev/null +++ b/server/service/transport_setup.go @@ -0,0 +1,16 @@ +package service + +import ( + "encoding/json" + "net/http" + + "golang.org/x/net/context" +) + +func decodeSetupRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req setupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil +} diff --git a/server/service/validation_setup.go b/server/service/validation_setup.go new file mode 100644 index 0000000000..e403702cb1 --- /dev/null +++ b/server/service/validation_setup.go @@ -0,0 +1,37 @@ +package service + +import ( + "fmt" + "net/url" + + "github.com/kolide/kolide-ose/server/kolide" + "golang.org/x/net/context" +) + +func (mw validationMiddleware) NewAppConfig(ctx context.Context, payload kolide.AppConfigPayload) (*kolide.AppConfig, error) { + invalid := &invalidArgumentError{} + var serverURLString string + if payload.ServerSettings == nil { + invalid.Append("kolide_server_url", "missing required argument") + } else { + serverURLString = *payload.ServerSettings.KolideServerURL + } + if err := validateKolideServerURL(serverURLString); err != nil { + invalid.Append("kolide_server_url", err.Error()) + } + if invalid.HasErrors() { + return nil, invalid + } + return mw.Service.NewAppConfig(ctx, payload) +} + +func validateKolideServerURL(urlString string) error { + serverURL, err := url.Parse(urlString) + if err != nil { + return err + } + if serverURL.Scheme != "https" { + return fmt.Errorf("url scheme must be https") + } + return nil +}