3
0

359 lines
9.2 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/pkg/options"
"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
webappsConf options.WebappOpt
// 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{}, webappsConf options.WebappOpt) *settings {
svc := &settings{
actionlog: al,
store: s,
accessControl: ac,
logger: log.Named("settings"),
current: current,
webappsConf: webappsConf,
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, 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
}
for _, v := range vv {
// if any of webapps stylesheet settings is updated, we need to recompute and update affected theme css
if v.Name == "ui.studio.themes" || v.Name == "ui.studio.custom-css" {
var compStyles *types.SettingValue
if v.Name == "ui.studio.themes" {
compStyles = current.FindByName("ui.studio.custom-css")
} else {
compStyles = current.FindByName("ui.studio.themes")
}
DefaultStylesheet.updateCSS(v, current.FindByName(v.Name), compStyles, v.Name, svc.webappsConf.ScssDirPath, svc.logger)
}
svc.logChange(ctx, v)
}
return svc.updateCurrent(ctx, vv)
}
func (svc *settings) logChange(ctx context.Context, v *types.SettingValue) {
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
}