Add settings service to messaging

This commit is contained in:
Tomaž Jerman
2019-10-16 12:37:37 +02:00
parent c686465e2b
commit 8aee952aa9
13 changed files with 851 additions and 2 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS `messaging_settings` (
rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',
name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',
value JSON COMMENT 'Setting value',
updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',
updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',
PRIMARY KEY (name, rel_owner)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+139
View File
@@ -0,0 +1,139 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `settings.go`, `settings.util.go` or `settings_test.go` to
implement your API calls, helper functions and tests. The file `settings.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/titpetric/factory/resputil"
"github.com/cortezaproject/corteza-server/messaging/rest/request"
"github.com/cortezaproject/corteza-server/pkg/logger"
)
// Internal API interface
type SettingsAPI interface {
List(context.Context, *request.SettingsList) (interface{}, error)
Update(context.Context, *request.SettingsUpdate) (interface{}, error)
Get(context.Context, *request.SettingsGet) (interface{}, error)
Set(context.Context, *request.SettingsSet) (interface{}, error)
}
// HTTP API interface
type Settings struct {
List func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Get func(http.ResponseWriter, *http.Request)
Set func(http.ResponseWriter, *http.Request)
}
func NewSettings(h SettingsAPI) *Settings {
return &Settings{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsList()
if err := params.Fill(r); err != nil {
logger.LogParamError("Settings.List", r, err)
resputil.JSON(w, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
logger.LogControllerError("Settings.List", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Settings.List", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsUpdate()
if err := params.Fill(r); err != nil {
logger.LogParamError("Settings.Update", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
logger.LogControllerError("Settings.Update", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Settings.Update", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Get: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsGet()
if err := params.Fill(r); err != nil {
logger.LogParamError("Settings.Get", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Get(r.Context(), params)
if err != nil {
logger.LogControllerError("Settings.Get", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Settings.Get", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
Set: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsSet()
if err := params.Fill(r); err != nil {
logger.LogParamError("Settings.Set", r, err)
resputil.JSON(w, err)
return
}
value, err := h.Set(r.Context(), params)
if err != nil {
logger.LogControllerError("Settings.Set", r, err, params.Auditable())
resputil.JSON(w, err)
return
}
logger.LogControllerCall("Settings.Set", r, params.Auditable())
if !serveHTTP(value, w, r) {
resputil.JSON(w, value)
}
},
}
}
func (h Settings) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/settings/", h.List)
r.Patch("/settings/", h.Update)
r.Get("/settings/{key}", h.Get)
r.Put("/settings/{key}", h.Set)
})
}
+262
View File
@@ -0,0 +1,262 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `settings.go`, `settings.util.go` or `settings_test.go` to
implement your API calls, helper functions and tests. The file `settings.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"io"
"strings"
"encoding/json"
"mime/multipart"
"net/http"
"github.com/go-chi/chi"
"github.com/pkg/errors"
sqlxTypes "github.com/jmoiron/sqlx/types"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// Settings list request parameters
type SettingsList struct {
Prefix string
}
func NewSettingsList() *SettingsList {
return &SettingsList{}
}
func (r SettingsList) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["prefix"] = r.Prefix
return out
}
func (r *SettingsList) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["prefix"]; ok {
r.Prefix = val
}
return err
}
var _ RequestFiller = NewSettingsList()
// Settings update request parameters
type SettingsUpdate struct {
Values sqlxTypes.JSONText
}
func NewSettingsUpdate() *SettingsUpdate {
return &SettingsUpdate{}
}
func (r SettingsUpdate) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["values"] = r.Values
return out
}
func (r *SettingsUpdate) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := post["values"]; ok {
if r.Values, err = parseJSONTextWithErr(val); err != nil {
return err
}
}
return err
}
var _ RequestFiller = NewSettingsUpdate()
// Settings get request parameters
type SettingsGet struct {
OwnerID uint64 `json:",string"`
Key string
}
func NewSettingsGet() *SettingsGet {
return &SettingsGet{}
}
func (r SettingsGet) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["ownerID"] = r.OwnerID
out["key"] = r.Key
return out
}
func (r *SettingsGet) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["ownerID"]; ok {
r.OwnerID = parseUInt64(val)
}
r.Key = chi.URLParam(req, "key")
return err
}
var _ RequestFiller = NewSettingsGet()
// Settings set request parameters
type SettingsSet struct {
Key string
OwnerID uint64 `json:",string"`
Value sqlxTypes.JSONText
}
func NewSettingsSet() *SettingsSet {
return &SettingsSet{}
}
func (r SettingsSet) Auditable() map[string]interface{} {
var out = map[string]interface{}{}
out["key"] = r.Key
out["ownerID"] = r.OwnerID
out["value"] = r.Value
return out
}
func (r *SettingsSet) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = req.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := req.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := req.Form
for name, param := range postVars {
post[name] = string(param[0])
}
r.Key = chi.URLParam(req, "key")
if val, ok := post["ownerID"]; ok {
r.OwnerID = parseUInt64(val)
}
if val, ok := post["value"]; ok {
if r.Value, err = parseJSONTextWithErr(val); err != nil {
return err
}
}
return err
}
var _ RequestFiller = NewSettingsSet()
+6
View File
@@ -13,6 +13,12 @@ import (
var truthy = regexp.MustCompile(`^\s*(t(rue)?|y(es)?|1)\s*$`)
func parseJSONTextWithErr(s string) (types.JSONText, error) {
result := &types.JSONText{}
err := errors.Wrap(result.Scan(s), "error when parsing JSONText")
return *result, err
}
func parseJSONText(s string) (types.JSONText, error) {
result := &types.JSONText{}
err := errors.Wrap(result.Scan(s), "error when parsing JSONText")
+1
View File
@@ -27,5 +27,6 @@ func MountRoutes(r chi.Router) {
handlers.NewCommands(Commands{}.New()).MountRoutes(r)
handlers.NewWebhooks(Webhooks{}.New()).MountRoutes(r)
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
handlers.NewSettings(Settings{}.New()).MountRoutes(r)
})
}
+61
View File
@@ -0,0 +1,61 @@
package rest
import (
"context"
"github.com/pkg/errors"
"github.com/titpetric/factory/resputil"
"github.com/cortezaproject/corteza-server/messaging/rest/request"
"github.com/cortezaproject/corteza-server/messaging/service"
"github.com/cortezaproject/corteza-server/pkg/settings"
)
var _ = errors.Wrap
type (
Settings struct {
svc struct {
settings service.SettingsService
}
}
)
func (Settings) New() *Settings {
ctrl := &Settings{}
ctrl.svc.settings = service.DefaultSettings
return ctrl
}
func (ctrl *Settings) List(ctx context.Context, r *request.SettingsList) (interface{}, error) {
if vv, err := ctrl.svc.settings.With(ctx).FindByPrefix(r.Prefix); err != nil {
return nil, err
} else {
return vv, err
}
}
func (ctrl *Settings) Update(ctx context.Context, r *request.SettingsUpdate) (interface{}, error) {
values := settings.ValueSet{}
if err := r.Values.Unmarshal(&values); err != nil {
return nil, err
} else if err := ctrl.svc.settings.With(ctx).BulkSet(values); err != nil {
return nil, err
} else {
return true, nil
}
}
func (ctrl *Settings) Get(ctx context.Context, r *request.SettingsGet) (interface{}, error) {
if v, err := ctrl.svc.settings.With(ctx).Get(r.Key, r.OwnerID); err != nil {
return nil, err
} else {
return v, nil
}
}
func (ctrl *Settings) Set(ctx context.Context, r *request.SettingsSet) (interface{}, error) {
return resputil.OK(), errors.New("Not implemented: Settings.set")
}
+8
View File
@@ -51,6 +51,14 @@ func (svc accessControl) CanGrant(ctx context.Context) bool {
return svc.can(ctx, types.MessagingPermissionResource, "grant")
}
func (svc accessControl) CanReadSettings(ctx context.Context) bool {
return svc.can(ctx, types.MessagingPermissionResource, "settings.read")
}
func (svc accessControl) CanManageSettings(ctx context.Context) bool {
return svc.can(ctx, types.MessagingPermissionResource, "settings.manage")
}
func (svc accessControl) CanCreatePublicChannel(ctx context.Context) bool {
return svc.can(ctx, types.MessagingPermissionResource, "channel.public.create", permissions.Allowed)
}
+8 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/cortezaproject/corteza-server/pkg/cli/options"
"github.com/cortezaproject/corteza-server/pkg/http"
"github.com/cortezaproject/corteza-server/pkg/permissions"
internalSettings "github.com/cortezaproject/corteza-server/pkg/settings"
"github.com/cortezaproject/corteza-server/pkg/store"
"github.com/cortezaproject/corteza-server/pkg/store/minio"
"github.com/cortezaproject/corteza-server/pkg/store/plain"
@@ -36,7 +37,9 @@ var (
DefaultLogger *zap.Logger
DefaultAccessControl *accessControl
DefaultInternalSettings internalSettings.Service
DefaultSettings SettingsService
DefaultAccessControl *accessControl
DefaultAttachment AttachmentService
DefaultChannel ChannelService
@@ -49,6 +52,8 @@ var (
func Init(ctx context.Context, log *zap.Logger, c Config) (err error) {
DefaultLogger = log.Named("service")
DefaultInternalSettings = internalSettings.NewService(internalSettings.NewRepository(repository.DB(ctx), "messaging_settings"))
if DefaultStore == nil {
if c.Storage.MinioEndpoint != "" {
if c.Storage.MinioBucket == "" {
@@ -97,6 +102,8 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) {
}
DefaultAccessControl = AccessControl(DefaultPermissions)
DefaultSettings = Settings(ctx, DefaultInternalSettings)
DefaultEvent = Event(ctx)
DefaultChannel = Channel(ctx)
DefaultAttachment = Attachment(ctx, DefaultStore)
+96
View File
@@ -0,0 +1,96 @@
package service
import (
"context"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/cortezaproject/corteza-server/messaging/repository"
"github.com/cortezaproject/corteza-server/pkg/logger"
internalSettings "github.com/cortezaproject/corteza-server/pkg/settings"
)
type (
// Wrapper service for messaging around internal settings service
settings struct {
ctx context.Context
db *factory.DB
logger *zap.Logger
ac settingsAccessController
internalSettings internalSettings.Service
}
settingsAccessController interface {
CanReadSettings(ctx context.Context) bool
CanManageSettings(ctx context.Context) bool
}
SettingsService interface {
With(ctx context.Context) SettingsService
FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error)
Set(v *internalSettings.Value) (err error)
BulkSet(vv internalSettings.ValueSet) (err error)
Get(name string, ownedBy uint64) (out *internalSettings.Value, err error)
}
)
func Settings(ctx context.Context, intSet internalSettings.Service) SettingsService {
return (&settings{
internalSettings: intSet,
ac: DefaultAccessControl,
logger: DefaultLogger.Named("settings"),
}).With(ctx)
}
func (svc settings) With(ctx context.Context) SettingsService {
db := repository.DB(ctx)
return &settings{
ctx: ctx,
db: db,
ac: svc.ac,
logger: svc.logger,
internalSettings: svc.internalSettings.With(ctx),
}
}
func (svc settings) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger {
return logger.AddRequestID(ctx, svc.logger).With(fields...)
}
func (svc settings) FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error) {
if !svc.ac.CanReadSettings(svc.ctx) {
return nil, errors.New("not allowed to read settings")
}
return svc.internalSettings.FindByPrefix(prefix)
}
func (svc settings) Set(v *internalSettings.Value) (err error) {
if !svc.ac.CanManageSettings(svc.ctx) {
return errors.New("not allowed to manage settings")
}
return svc.internalSettings.Set(v)
}
func (svc settings) BulkSet(vv internalSettings.ValueSet) (err error) {
if !svc.ac.CanManageSettings(svc.ctx) {
return errors.New("not allowed to manage settings")
}
return svc.internalSettings.BulkSet(vv)
}
func (svc settings) Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) {
if !svc.ac.CanReadSettings(svc.ctx) {
return nil, errors.New("not allowed to read settings")
}
return svc.internalSettings.Get(name, ownedBy)
}