diff --git a/cli/serve.go b/cli/serve.go index f5de1a7ad9..cfa9844041 100644 --- a/cli/serve.go +++ b/cli/serve.go @@ -115,6 +115,17 @@ the way that the kolide server works. initFatal(err, "creating bootstrap user") } } + devOrgInfo := &kolide.OrgInfo{ + OrgName: "Kolide", + OrgLogoURL: fmt.Sprintf("%s/logo.png", config.Server.Address), + } + _, err := svc.NewOrgInfo(ctx, kolide.OrgInfoPayload{ + OrgName: &devOrgInfo.OrgName, + OrgLogoURL: &devOrgInfo.OrgLogoURL, + }) + if err != nil { + initFatal(err, "creating fake org info") + } } svcLogger := kitlog.NewContext(logger).With("component", "service") diff --git a/datastore/gorm.go b/datastore/gorm.go index 3e2788f1d9..d8462b405d 100644 --- a/datastore/gorm.go +++ b/datastore/gorm.go @@ -34,6 +34,7 @@ var tables = [...]interface{}{ &kolide.DistributedQueryCampaignTarget{}, &kolide.Query{}, &kolide.DistributedQueryExecution{}, + &kolide.OrgInfo{}, } type gormDB struct { diff --git a/datastore/gorm_app.go b/datastore/gorm_app.go new file mode 100644 index 0000000000..26d8a6a9f7 --- /dev/null +++ b/datastore/gorm_app.go @@ -0,0 +1,35 @@ +package datastore + +import ( + "github.com/jinzhu/gorm" + "github.com/kolide/kolide-ose/kolide" +) + +func (orm gormDB) NewOrgInfo(info *kolide.OrgInfo) (*kolide.OrgInfo, error) { + err := orm.DB.First(info).Error + switch err { + case gorm.ErrRecordNotFound: + err = orm.DB.Create(info).Error + if err != nil { + return nil, err + } + return info, nil + case nil: + return info, orm.SaveOrgInfo(info) + default: + return nil, err + } +} + +func (orm gormDB) OrgInfo() (*kolide.OrgInfo, error) { + info := &kolide.OrgInfo{} + err := orm.DB.First(info).Error + if err != nil { + return nil, err + } + return info, nil +} + +func (orm gormDB) SaveOrgInfo(info *kolide.OrgInfo) error { + return orm.DB.Save(info).Error +} diff --git a/datastore/gorm_app_test.go b/datastore/gorm_app_test.go new file mode 100644 index 0000000000..0ce05e7fd1 --- /dev/null +++ b/datastore/gorm_app_test.go @@ -0,0 +1,50 @@ +package datastore + +import ( + "os" + "testing" + + "github.com/kolide/kolide-ose/kolide" + "github.com/stretchr/testify/assert" +) + +func TestOrgInfo(t *testing.T) { + var ds kolide.Datastore + address := os.Getenv("MYSQL_ADDR") + if address == "" { + ds = setup(t) + } else { + ds = setupMySQLGORM(t) + defer teardownMySQLGORM(t, ds) + } + + testOrgInfo(t, ds) +} + +func testOrgInfo(t *testing.T, db kolide.Datastore) { + info := &kolide.OrgInfo{ + OrgName: "Kolide", + OrgLogoURL: "localhost:8080/logo.png", + } + + info, err := db.NewOrgInfo(info) + assert.Nil(t, err) + assert.Equal(t, info.ID, uint(1)) + + info2, err := db.OrgInfo() + assert.Nil(t, err) + assert.Equal(t, info2.ID, uint(1)) + assert.Equal(t, info2.OrgName, info.OrgName) + + info2.OrgName = "koolide" + err = db.SaveOrgInfo(info2) + assert.Nil(t, err) + + info3, err := db.OrgInfo() + assert.Nil(t, err) + assert.Equal(t, info3.OrgName, info2.OrgName) + + info4, err := db.NewOrgInfo(info3) + assert.Nil(t, err) + assert.Equal(t, info4.ID, uint(1)) +} diff --git a/datastore/inmem.go b/datastore/inmem.go index 5be2dcc0d6..62ddd1bfb5 100644 --- a/datastore/inmem.go +++ b/datastore/inmem.go @@ -13,6 +13,7 @@ type inmem struct { users map[uint]*kolide.User sessions map[uint]*kolide.Session passwordResets map[uint]*kolide.PasswordResetRequest + orginfo *kolide.OrgInfo } func (orm *inmem) Name() string { diff --git a/datastore/inmem_app.go b/datastore/inmem_app.go new file mode 100644 index 0000000000..2c6429fc89 --- /dev/null +++ b/datastore/inmem_app.go @@ -0,0 +1,30 @@ +package datastore + +import "github.com/kolide/kolide-ose/kolide" + +func (orm *inmem) NewOrgInfo(info *kolide.OrgInfo) (*kolide.OrgInfo, error) { + orm.mtx.Lock() + defer orm.mtx.Unlock() + + orm.orginfo = info + return info, nil +} + +func (orm *inmem) OrgInfo() (*kolide.OrgInfo, error) { + orm.mtx.Lock() + defer orm.mtx.Unlock() + + if orm.orginfo != nil { + return orm.orginfo, nil + } + + return nil, ErrNotFound +} + +func (orm *inmem) SaveOrgInfo(info *kolide.OrgInfo) error { + orm.mtx.Lock() + defer orm.mtx.Unlock() + + orm.orginfo = info + return nil +} diff --git a/kolide/app.go b/kolide/app.go new file mode 100644 index 0000000000..8b2df391a5 --- /dev/null +++ b/kolide/app.go @@ -0,0 +1,34 @@ +package kolide + +import "context" + +// AppConfigStore contains method for saving and retrieving +// application configuration +type AppConfigStore interface { + NewOrgInfo(info *OrgInfo) (*OrgInfo, error) + OrgInfo() (*OrgInfo, error) + SaveOrgInfo(info *OrgInfo) error +} + +// AppConfigService provides methods for configuring +// the Kolide application +type AppConfigService interface { + NewOrgInfo(ctx context.Context, p OrgInfoPayload) (*OrgInfo, error) + OrgInfo(ctx context.Context) (*OrgInfo, error) + ModifyOrgInfo(ctx context.Context, p OrgInfoPayload) (*OrgInfo, error) +} + +// OrgInfo holds information about the current +// organization using Kolide +type OrgInfo struct { + ID uint `gorm:"primary_key"` + OrgName string + OrgLogoURL string +} + +// OrgInfoPayload is used to accept +// OrgInfo modifications by a client +type OrgInfoPayload struct { + OrgName *string `json:"org_name"` + OrgLogoURL *string `json:"org_logo_url"` +} diff --git a/kolide/datastore.go b/kolide/datastore.go index bbed1bcda5..96b91a7a33 100644 --- a/kolide/datastore.go +++ b/kolide/datastore.go @@ -9,6 +9,7 @@ type Datastore interface { HostStore PasswordResetStore SessionStore + AppConfigStore Name() string Drop() error Migrate() error diff --git a/kolide/service.go b/kolide/service.go index 90a0bb04b0..d8c9c2ff02 100644 --- a/kolide/service.go +++ b/kolide/service.go @@ -8,4 +8,5 @@ type Service interface { QueryService OsqueryService HostService + AppConfigService } diff --git a/server/endpoint_appconfig.go b/server/endpoint_appconfig.go new file mode 100644 index 0000000000..db3b68fd7a --- /dev/null +++ b/server/endpoint_appconfig.go @@ -0,0 +1,54 @@ +package server + +import ( + "github.com/go-kit/kit/endpoint" + "github.com/kolide/kolide-ose/kolide" + "golang.org/x/net/context" +) + +// getAppConfig is used to return +// current configuration data to the client +type getAppConfigResponse struct { + Err error `json:"error,omitempty"` +} + +func (r getAppConfigResponse) error() error { return r.Err } + +type appConfig map[string]map[string]string + +func makeGetAppConfigEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + info, err := svc.OrgInfo(ctx) + if err != nil { + return getAppConfigResponse{Err: err}, nil + } + config := appConfig{ + "org_info": map[string]string{ + "org_name": info.OrgName, + "org_logo_url": info.OrgLogoURL, + }, + } + return config, nil + } +} + +type modifyAppConfigRequest struct { + OrgPayload kolide.OrgInfoPayload `json:"org_info"` +} + +func makeModifyAppConfigRequest(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(modifyAppConfigRequest) + info, err := svc.ModifyOrgInfo(ctx, req.OrgPayload) + if err != nil { + return getAppConfigResponse{Err: err}, nil + } + config := appConfig{ + "org_info": map[string]string{ + "org_name": info.OrgName, + "org_logo_url": info.OrgLogoURL, + }, + } + return config, nil + } +} diff --git a/server/handler.go b/server/handler.go index ed7d99d9fe..f455573b87 100644 --- a/server/handler.go +++ b/server/handler.go @@ -141,6 +141,26 @@ func attachAPIRoutes(router *mux.Router, ctx context.Context, svc kolide.Service ), ).Methods("DELETE") + router.Handle("/api/v1/kolide/config", + kithttp.NewServer( + ctx, + authenticated(makeGetAppConfigEndpoint(svc)), + decodeNoParamsRequest, + encodeResponse, + opts..., + ), + ).Methods("GET") + + router.Handle("/api/v1/kolide/config", + kithttp.NewServer( + ctx, + authenticated(mustBeAdmin(makeModifyAppConfigRequest(svc))), + decodeModifyAppConfigRequest, + encodeResponse, + opts..., + ), + ).Methods("PATCH") + router.Handle("/api/v1/kolide/queries/{id}", kithttp.NewServer( ctx, diff --git a/server/handler_test.go b/server/handler_test.go index 9954127ab6..7f97602af9 100644 --- a/server/handler_test.go +++ b/server/handler_test.go @@ -61,6 +61,14 @@ func TestAPIRoutes(t *testing.T) { verb: "GET", uri: "/api/v1/kolide/me", }, + { + verb: "GET", + uri: "/api/v1/kolide/config", + }, + { + verb: "PATCH", + uri: "/api/v1/kolide/config", + }, { verb: "GET", uri: "/api/v1/kolide/queries/1", diff --git a/server/service_appconfig.go b/server/service_appconfig.go new file mode 100644 index 0000000000..135c157bf8 --- /dev/null +++ b/server/service_appconfig.go @@ -0,0 +1,46 @@ +package server + +import ( + "context" + + "github.com/kolide/kolide-ose/kolide" +) + +func (svc service) NewOrgInfo(ctx context.Context, p kolide.OrgInfoPayload) (*kolide.OrgInfo, error) { + info := &kolide.OrgInfo{} + if p.OrgName != nil { + info.OrgName = *p.OrgName + } + if p.OrgLogoURL != nil { + info.OrgLogoURL = *p.OrgLogoURL + } + info, err := svc.ds.NewOrgInfo(info) + if err != nil { + return nil, err + } + return info, nil +} + +func (svc service) OrgInfo(ctx context.Context) (*kolide.OrgInfo, error) { + return svc.ds.OrgInfo() +} + +func (svc service) ModifyOrgInfo(ctx context.Context, p kolide.OrgInfoPayload) (*kolide.OrgInfo, error) { + info, err := svc.ds.OrgInfo() + if err != nil { + return nil, err + } + + if p.OrgName != nil { + info.OrgName = *p.OrgName + } + if p.OrgLogoURL != nil { + info.OrgLogoURL = *p.OrgLogoURL + } + + err = svc.ds.SaveOrgInfo(info) + if err != nil { + return nil, err + } + return info, nil +} diff --git a/server/transport_appconfig.go b/server/transport_appconfig.go new file mode 100644 index 0000000000..1af6b8485e --- /dev/null +++ b/server/transport_appconfig.go @@ -0,0 +1,16 @@ +package server + +import ( + "encoding/json" + "net/http" + + "golang.org/x/net/context" +) + +func decodeModifyAppConfigRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req modifyAppConfigRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil +}