This is due to us introducing the web console and the uints needing to be string encoded (because of JavaScript).
344 lines
8.6 KiB
Go
344 lines
8.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cortezaproject/corteza/server/pkg/actionlog"
|
|
"github.com/spf13/cast"
|
|
|
|
"github.com/cortezaproject/corteza/server/pkg/errors"
|
|
"github.com/cortezaproject/corteza/server/pkg/logger"
|
|
"github.com/cortezaproject/corteza/server/store"
|
|
"github.com/cortezaproject/corteza/server/system/types"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
type (
|
|
settings struct {
|
|
actionlog actionlog.Recorder
|
|
store store.SettingValues
|
|
accessControl accessController
|
|
logger *zap.Logger
|
|
m sync.RWMutex
|
|
|
|
// Holds reference to the "current" settings that
|
|
// are used by the services
|
|
current interface{}
|
|
|
|
// update channel we'll write set of changed settings when change occurs
|
|
update chan types.SettingValueSet
|
|
|
|
listeners []registeredSettingsListener
|
|
}
|
|
|
|
accessController interface {
|
|
CanReadSettings(ctx context.Context) bool
|
|
CanManageSettings(ctx context.Context) bool
|
|
}
|
|
|
|
SettingsChangeListener func(ctx context.Context, current interface{}, set types.SettingValueSet)
|
|
registeredSettingsListener struct {
|
|
prefix string
|
|
fn SettingsChangeListener
|
|
}
|
|
)
|
|
|
|
func Settings(ctx context.Context, s store.SettingValues, log *zap.Logger, ac accessController, al actionlog.Recorder, current interface{}) *settings {
|
|
svc := &settings{
|
|
actionlog: al,
|
|
store: s,
|
|
accessControl: ac,
|
|
logger: log.Named("settings"),
|
|
current: current,
|
|
listeners: make([]registeredSettingsListener, 0, 2),
|
|
update: make(chan types.SettingValueSet, 0),
|
|
}
|
|
|
|
svc.watch(ctx)
|
|
|
|
return svc
|
|
}
|
|
|
|
func (svc *settings) Register(prefix string, listener SettingsChangeListener) {
|
|
svc.logger.Debug("registering new settings change listener", zap.String("prefix", prefix))
|
|
|
|
svc.m.Lock()
|
|
defer svc.m.Unlock()
|
|
|
|
svc.listeners = append(svc.listeners, registeredSettingsListener{
|
|
prefix: prefix,
|
|
fn: listener,
|
|
})
|
|
}
|
|
|
|
func (svc *settings) watch(ctx context.Context) {
|
|
go func() {
|
|
for {
|
|
select {
|
|
case vv := <-svc.update:
|
|
svc.notify(ctx, vv)
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (svc *settings) notify(ctx context.Context, vv types.SettingValueSet) {
|
|
svc.m.RLock()
|
|
defer svc.m.RUnlock()
|
|
|
|
for _, l := range svc.listeners {
|
|
go func(l registeredSettingsListener, vv types.SettingValueSet) {
|
|
if len(l.prefix) > 0 {
|
|
vv = vv.FilterByPrefix(l.prefix)
|
|
}
|
|
|
|
svc.logger.Debug(
|
|
"notifying listener",
|
|
zap.String("prefix", l.prefix),
|
|
zap.Int("changes", len(vv)),
|
|
)
|
|
|
|
if len(vv) > 0 {
|
|
l.fn(ctx, svc.current, vv)
|
|
}
|
|
}(l, vv)
|
|
}
|
|
}
|
|
|
|
func (svc *settings) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger {
|
|
return logger.AddRequestID(ctx, svc.logger).With(fields...)
|
|
}
|
|
|
|
func (svc *settings) FindByPrefix(ctx context.Context, pp ...string) (types.SettingValueSet, error) {
|
|
if !svc.accessControl.CanReadSettings(ctx) {
|
|
return nil, SettingsErrNotAllowedToRead()
|
|
}
|
|
|
|
return svc.findByPrefix(ctx, pp...)
|
|
}
|
|
|
|
func (svc *settings) findByPrefix(ctx context.Context, pp ...string) (types.SettingValueSet, error) {
|
|
var (
|
|
f = types.SettingsFilter{
|
|
Prefix: strings.Join(pp, "."),
|
|
Check: func(*types.SettingValue) (bool, error) { return true, nil },
|
|
}
|
|
)
|
|
|
|
vv, _, err := store.SearchSettingValues(ctx, svc.store, f)
|
|
return vv, err
|
|
}
|
|
|
|
func (svc *settings) Get(ctx context.Context, name string, ownedBy uint64) (out *types.SettingValue, err error) {
|
|
if !svc.accessControl.CanReadSettings(ctx) {
|
|
return nil, SettingsErrNotAllowedToRead()
|
|
}
|
|
|
|
out, err = store.LookupSettingValueByNameOwnedBy(ctx, svc.store, name, ownedBy)
|
|
if err != nil && !errors.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (svc *settings) UpdateCurrent(ctx context.Context) error {
|
|
if vv, err := svc.findByPrefix(ctx); err != nil {
|
|
return err
|
|
} else {
|
|
return svc.updateCurrent(ctx, vv)
|
|
}
|
|
}
|
|
|
|
func (svc *settings) updateCurrent(ctx context.Context, vv types.SettingValueSet) (err error) {
|
|
// update current settings with new values
|
|
if err = vv.KV().Decode(svc.current); err != nil {
|
|
return fmt.Errorf("could not decode settings into KV: %w", err)
|
|
}
|
|
|
|
// push message over update chan so that we can notify all settings listeners
|
|
select {
|
|
case svc.update <- vv:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(time.Second):
|
|
svc.logger.Warn("timed-out while waiting to update current settings")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc *settings) Set(ctx context.Context, v *types.SettingValue) (err error) {
|
|
if !svc.accessControl.CanManageSettings(ctx) {
|
|
return SettingsErrNotAllowedToManage()
|
|
}
|
|
|
|
if _, err = check(v); err != nil {
|
|
return
|
|
}
|
|
|
|
var current *types.SettingValue
|
|
current, err = store.LookupSettingValueByNameOwnedBy(ctx, svc.store, v.Name, v.OwnedBy)
|
|
if errors.IsNotFound(err) {
|
|
v.UpdatedAt = *now()
|
|
err = store.CreateSettingValue(ctx, svc.store, v)
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !current.Eq(v) {
|
|
v.UpdatedAt = *now()
|
|
err = store.UpdateSettingValue(ctx, svc.store, v)
|
|
}
|
|
|
|
if err != nil || current.Eq(v) {
|
|
// Return on error or when there is nothing to update (same value)
|
|
return
|
|
}
|
|
|
|
svc.logChange(ctx, types.SettingValueSet{v})
|
|
return svc.updateCurrent(ctx, types.SettingValueSet{v})
|
|
}
|
|
|
|
func (svc *settings) BulkSet(ctx context.Context, vv types.SettingValueSet) (err error) {
|
|
if !svc.accessControl.CanManageSettings(ctx) {
|
|
return SettingsErrNotAllowedToManage()
|
|
}
|
|
|
|
if _, err = check(vv...); err != nil {
|
|
return
|
|
}
|
|
|
|
// Load current settings and get changed values
|
|
var current, old, new types.SettingValueSet
|
|
if current, err = svc.FindByPrefix(ctx); err != nil {
|
|
return
|
|
} else {
|
|
vv = current.Changed(vv)
|
|
_ = vv.Walk(func(v *types.SettingValue) error {
|
|
v.UpdatedAt = *now()
|
|
return nil
|
|
})
|
|
|
|
old = current.Old(vv)
|
|
new = current.New(vv)
|
|
}
|
|
|
|
if err = store.UpdateSettingValue(ctx, svc.store, old...); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = store.CreateSettingValue(ctx, svc.store, new...); err != nil {
|
|
return
|
|
}
|
|
|
|
if err = store.DeleteSettingValue(ctx, svc.store, vv.Trash()...); err != nil {
|
|
return
|
|
}
|
|
|
|
svc.logChange(ctx, vv)
|
|
|
|
return svc.updateCurrent(ctx, vv)
|
|
}
|
|
|
|
func (svc *settings) logChange(ctx context.Context, vv types.SettingValueSet) {
|
|
for _, v := range vv {
|
|
svc.log(ctx,
|
|
zap.String("name", v.Name),
|
|
logger.Uint64("owned-by", v.OwnedBy),
|
|
zap.Stringer("value", v.Value)).
|
|
WithOptions(zap.AddCallerSkip(1)).
|
|
Debug("setting value updated")
|
|
}
|
|
}
|
|
|
|
func (svc *settings) Delete(ctx context.Context, name string, ownedBy uint64) error {
|
|
if !svc.accessControl.CanManageSettings(ctx) {
|
|
return SettingsErrNotAllowedToManage()
|
|
}
|
|
|
|
current, err := store.LookupSettingValueByNameOwnedBy(ctx, svc.store, name, ownedBy)
|
|
if errors.IsNotFound(err) {
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = store.DeleteSettingValue(ctx, svc.store, current)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
vv := types.SettingValueSet{current}
|
|
|
|
svc.log(ctx,
|
|
zap.String("name", name),
|
|
logger.Uint64("owned-by", ownedBy)).Info("setting value removed")
|
|
|
|
return svc.updateCurrent(ctx, vv)
|
|
}
|
|
|
|
// Check validates settings values
|
|
func check(vv ...*types.SettingValue) (ok bool, err error) {
|
|
set := append(types.SettingValueSet{}, vv...)
|
|
err = set.Walk(func(val *types.SettingValue) error {
|
|
if val == nil || val.Value == nil {
|
|
return err
|
|
}
|
|
|
|
vs := val.String()
|
|
if len(vs) == 0 && len(val.Value.String()) > 0 {
|
|
vs = val.Value.String()
|
|
}
|
|
|
|
// Password constraints: The min password length should be 8
|
|
if val.Name == "auth.internal.password-constraints.min-length" {
|
|
if cast.ToInt(vs) < passwordMinLength {
|
|
return SettingsErrInvalidPasswordMinLength()
|
|
}
|
|
}
|
|
|
|
// Password constraints: The min upper case count should not be a negative number
|
|
if val.Name == "auth.internal.password-constraints.min-upper-case" {
|
|
if cast.ToInt(vs) < 0 {
|
|
return SettingsErrInvalidPasswordMinUpperCase()
|
|
}
|
|
}
|
|
|
|
// Password constraints: The min lower case count should not be a negative number
|
|
if val.Name == "auth.internal.password-constraints.min-lower-case" {
|
|
if cast.ToInt(vs) < 0 {
|
|
return SettingsErrInvalidPasswordMinLowerCase()
|
|
}
|
|
}
|
|
|
|
// Password constraints: The min number of numeric characters should not be a negative number
|
|
if val.Name == "auth.internal.password-constraints.min-num-count" {
|
|
if cast.ToInt(vs) < 0 {
|
|
return SettingsErrInvalidPasswordMinNumCount()
|
|
}
|
|
}
|
|
|
|
// Password constraints: The min number of special characters should not be a negative number
|
|
if val.Name == "auth.internal.password-constraints.min-special-count" {
|
|
if cast.ToInt(vs) < 0 {
|
|
return SettingsErrInvalidPasswordMinSpecialCharCount()
|
|
}
|
|
}
|
|
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|