App config api (#223)

Add GET and PATCH endpoints for /kolide/config to get/update current app configuration
This commit is contained in:
Victor Vrantchan 2016-09-21 20:45:57 -04:00 committed by GitHub
parent a3e26b6b9a
commit 26b1e70ac3
14 changed files with 308 additions and 0 deletions

View File

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

View File

@ -34,6 +34,7 @@ var tables = [...]interface{}{
&kolide.DistributedQueryCampaignTarget{},
&kolide.Query{},
&kolide.DistributedQueryExecution{},
&kolide.OrgInfo{},
}
type gormDB struct {

35
datastore/gorm_app.go Normal file
View File

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

View File

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

View File

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

30
datastore/inmem_app.go Normal file
View File

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

34
kolide/app.go Normal file
View File

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

View File

@ -9,6 +9,7 @@ type Datastore interface {
HostStore
PasswordResetStore
SessionStore
AppConfigStore
Name() string
Drop() error
Migrate() error

View File

@ -8,4 +8,5 @@ type Service interface {
QueryService
OsqueryService
HostService
AppConfigService
}

View File

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

View File

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

View File

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

View File

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

View File

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