mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
create first time setup endpoint (#436)
The endpoint is only active if there are no users in the datastore.
While the endpoint is active, it also disables all the other API endpoints, and /config returns `{"require_setup":true}`
for #378
This commit is contained in:
parent
0388297fd8
commit
ac14215e21
9 changed files with 189 additions and 14 deletions
13
cli/serve.go
13
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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
60
server/service/endpoint_setup.go
Normal file
60
server/service/endpoint_setup.go
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
16
server/service/transport_setup.go
Normal file
16
server/service/transport_setup.go
Normal file
|
|
@ -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
|
||||
}
|
||||
37
server/service/validation_setup.go
Normal file
37
server/service/validation_setup.go
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in a new issue