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:
Victor Vrantchan 2016-11-09 12:19:07 -05:00 committed by GitHub
parent 0388297fd8
commit ac14215e21
9 changed files with 189 additions and 14 deletions

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

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