3
0

Add validation for password while updating system settings

Also, adds setting actions file and updated all settings errors to translated error
This commit is contained in:
Vivek Patel
2022-06-23 23:51:36 +05:30
parent 8ec05a032c
commit 12fd426ec7
6 changed files with 683 additions and 21 deletions

View File

@@ -150,7 +150,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock
DefaultAccessControl = AccessControl(s)
DefaultSettings = Settings(ctx, DefaultStore, DefaultLogger, DefaultAccessControl, CurrentSettings)
DefaultSettings = Settings(ctx, DefaultStore, DefaultLogger, DefaultAccessControl, DefaultActionlog, CurrentSettings)
DefaultDalConnection = Connection(ctx, dal.Service(), c.DB)

498
system/service/setting_actions.gen.go generated Normal file
View File

@@ -0,0 +1,498 @@
package service
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// system/service/setting_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/locale"
"github.com/cortezaproject/corteza-server/system/types"
"strings"
"time"
)
type (
settingsActionProps struct {
settings *types.SettingValue
}
settingsAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *settingsActionProps
}
settingsLogMetaKey struct{}
settingsPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setSettings updates settingsActionProps's settings
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *settingsActionProps) setSettings(settings *types.SettingValue) *settingsActionProps {
p.settings = settings
return p
}
// Serialize converts settingsActionProps to actionlog.Meta
//
// This function is auto-generated.
//
func (p settingsActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.settings != nil {
m.Set("settings.name", p.settings.Name, true)
m.Set("settings.value", p.settings.Value, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
//
func (p settingsActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.settings != nil {
// replacement for "{{settings}}" (in order how fields are defined)
pairs = append(
pairs,
"{{settings}}",
fns(
p.settings.Name,
p.settings.Value,
),
)
pairs = append(pairs, "{{settings.name}}", fns(p.settings.Name))
pairs = append(pairs, "{{settings.value}}", fns(p.settings.Value))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
//
func (a *settingsAction) String() string {
var props = &settingsActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *settingsAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// SettingsActionLookup returns "system:setting.lookup" action
//
// This function is auto-generated.
//
func SettingsActionLookup(props ...*settingsActionProps) *settingsAction {
a := &settingsAction{
timestamp: time.Now(),
resource: "system:setting",
action: "lookup",
log: "looked-up for a setting",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// SettingsErrGeneric returns "system:setting.generic" as *errors.Error
//
//
// This function is auto-generated.
//
func SettingsErrGeneric(mm ...*settingsActionProps) *errors.Error {
var p = &settingsActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "system:setting"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(settingsLogMetaKey{}, "{err}"),
errors.Meta(settingsPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "settings.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SettingsErrNotAllowedToRead returns "system:setting.notAllowedToRead" as *errors.Error
//
//
// This function is auto-generated.
//
func SettingsErrNotAllowedToRead(mm ...*settingsActionProps) *errors.Error {
var p = &settingsActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to read this setting", nil),
errors.Meta("type", "notAllowedToRead"),
errors.Meta("resource", "system:setting"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(settingsLogMetaKey{}, "failed to read {{settings.name}}; insufficient permissions"),
errors.Meta(settingsPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "settings.errors.notAllowedToRead"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SettingsErrNotAllowedToManage returns "system:setting.notAllowedToManage" as *errors.Error
//
//
// This function is auto-generated.
//
func SettingsErrNotAllowedToManage(mm ...*settingsActionProps) *errors.Error {
var p = &settingsActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to manage this setting", nil),
errors.Meta("type", "notAllowedToManage"),
errors.Meta("resource", "system:setting"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(settingsLogMetaKey{}, "failed to manage {{settings.name}}; insufficient permissions"),
errors.Meta(settingsPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "settings.errors.notAllowedToManage"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SettingsErrInvalidPasswordMinLength returns "system:setting.invalidPasswordMinLength" as *errors.Error
//
//
// This function is auto-generated.
//
func SettingsErrInvalidPasswordMinLength(mm ...*settingsActionProps) *errors.Error {
var p = &settingsActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("password constraint minimum length should be at least 8 characters", nil),
errors.Meta("type", "invalidPasswordMinLength"),
errors.Meta("resource", "system:setting"),
errors.Meta(settingsPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "settings.errors.invalidPasswordMinLength"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SettingsErrInvalidPasswordMinUpperCase returns "system:setting.invalidPasswordMinUpperCase" as *errors.Error
//
//
// This function is auto-generated.
//
func SettingsErrInvalidPasswordMinUpperCase(mm ...*settingsActionProps) *errors.Error {
var p = &settingsActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("password constraint minimum upper case count should not be a negative number", nil),
errors.Meta("type", "invalidPasswordMinUpperCase"),
errors.Meta("resource", "system:setting"),
errors.Meta(settingsPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "settings.errors.invalidPasswordMinUpperCase"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SettingsErrInvalidPasswordMinLowerCase returns "system:setting.invalidPasswordMinLowerCase" as *errors.Error
//
//
// This function is auto-generated.
//
func SettingsErrInvalidPasswordMinLowerCase(mm ...*settingsActionProps) *errors.Error {
var p = &settingsActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("password constraint minimum lower case count should not be a negative number", nil),
errors.Meta("type", "invalidPasswordMinLowerCase"),
errors.Meta("resource", "system:setting"),
errors.Meta(settingsPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "settings.errors.invalidPasswordMinLowerCase"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SettingsErrInvalidPasswordMinNumCount returns "system:setting.invalidPasswordMinNumCount" as *errors.Error
//
//
// This function is auto-generated.
//
func SettingsErrInvalidPasswordMinNumCount(mm ...*settingsActionProps) *errors.Error {
var p = &settingsActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("password constraint minimum number count should not be a negative number", nil),
errors.Meta("type", "invalidPasswordMinNumCount"),
errors.Meta("resource", "system:setting"),
errors.Meta(settingsPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "settings.errors.invalidPasswordMinNumCount"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SettingsErrInvalidPasswordMinSpecialCharCount returns "system:setting.invalidPasswordMinSpecialCharCount" as *errors.Error
//
//
// This function is auto-generated.
//
func SettingsErrInvalidPasswordMinSpecialCharCount(mm ...*settingsActionProps) *errors.Error {
var p = &settingsActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("password constraint minimum special character count should not be a negative number", nil),
errors.Meta("type", "invalidPasswordMinSpecialCharCount"),
errors.Meta("resource", "system:setting"),
errors.Meta(settingsPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "settings.errors.invalidPasswordMinSpecialCharCount"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
//
func (svc settings) recordAction(ctx context.Context, props *settingsActionProps, actionFn func(...*settingsActionProps) *settingsAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(settingsLogMetaKey{}), err)
if p, has := m[settingsPropsMetaKey{}]; has {
a.Meta = p.(*settingsActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,52 @@
# List of loggable service actions
resource: system:setting
service: settings
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza-server/system/types
props:
- name: settings
type: "*types.SettingValue"
fields: [ name, value ]
actions:
- action: lookup
log: "looked-up for a setting"
severity: info
errors:
- error: notAllowedToRead
message: "not allowed to read this setting"
log: "failed to read {{settings.name}}; insufficient permissions"
- error: notAllowedToManage
message: "not allowed to manage this setting"
log: "failed to manage {{settings.name}}; insufficient permissions"
- error: invalidPasswordMinLength
message: "password constraint minimum length should be at least 8 characters"
severity: warning
- error: invalidPasswordMinUpperCase
message: "password constraint minimum upper case count should not be a negative number"
severity: warning
- error: invalidPasswordMinLowerCase
message: "password constraint minimum lower case count should not be a negative number"
severity: warning
- error: invalidPasswordMinNumCount
message: "password constraint minimum number count should not be a negative number"
severity: warning
- error: invalidPasswordMinSpecialCharCount
message: "password constraint minimum special character count should not be a negative number"
severity: warning

View File

@@ -3,6 +3,8 @@ package service
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/spf13/cast"
"strings"
"sync"
"time"
@@ -17,6 +19,7 @@ import (
type (
settings struct {
actionlog actionlog.Recorder
store store.SettingValues
accessControl accessController
logger *zap.Logger
@@ -44,13 +47,9 @@ type (
}
)
var (
ErrNoReadPermission = fmt.Errorf("not allowed to read settings")
ErrNoManagePermission = fmt.Errorf("not allowed to manage settings")
)
func Settings(ctx context.Context, s store.SettingValues, log *zap.Logger, ac accessController, current interface{}) *settings {
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"),
@@ -118,7 +117,7 @@ func (svc *settings) log(ctx context.Context, fields ...zapcore.Field) *zap.Logg
func (svc *settings) FindByPrefix(ctx context.Context, pp ...string) (types.SettingValueSet, error) {
if !svc.accessControl.CanReadSettings(ctx) {
return nil, ErrNoReadPermission
return nil, SettingsErrNotAllowedToRead()
}
return svc.findByPrefix(ctx, pp...)
@@ -138,7 +137,7 @@ func (svc *settings) findByPrefix(ctx context.Context, pp ...string) (types.Sett
func (svc *settings) Get(ctx context.Context, name string, ownedBy uint64) (out *types.SettingValue, err error) {
if !svc.accessControl.CanReadSettings(ctx) {
return nil, ErrNoReadPermission
return nil, SettingsErrNotAllowedToRead()
}
out, err = store.LookupSettingValueByNameOwnedBy(ctx, svc.store, name, ownedBy)
@@ -177,7 +176,11 @@ func (svc *settings) updateCurrent(ctx context.Context, vv types.SettingValueSet
func (svc *settings) Set(ctx context.Context, v *types.SettingValue) (err error) {
if !svc.accessControl.CanManageSettings(ctx) {
return ErrNoManagePermission
return SettingsErrNotAllowedToManage()
}
if _, err = check(v); err != nil {
return
}
var current *types.SettingValue
@@ -205,7 +208,11 @@ func (svc *settings) Set(ctx context.Context, v *types.SettingValue) (err error)
func (svc *settings) BulkSet(ctx context.Context, vv types.SettingValueSet) (err error) {
if !svc.accessControl.CanManageSettings(ctx) {
return ErrNoManagePermission
return SettingsErrNotAllowedToManage()
}
if _, err = check(vv...); err != nil {
return
}
// Load current settings and get changed values
@@ -253,7 +260,7 @@ func (svc *settings) logChange(ctx context.Context, vv types.SettingValueSet) {
func (svc *settings) Delete(ctx context.Context, name string, ownedBy uint64) error {
if !svc.accessControl.CanManageSettings(ctx) {
return ErrNoManagePermission
return SettingsErrNotAllowedToManage()
}
current, err := store.LookupSettingValueByNameOwnedBy(ctx, svc.store, name, ownedBy)
@@ -276,3 +283,51 @@ func (svc *settings) Delete(ctx context.Context, name string, ownedBy uint64) er
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 {
// Password constraints: The min password length should be 8
if val.Name == "auth.internal.password-constraints.min-length" {
if cast.ToUint64(val.String()) < 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(val.String()) < 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(val.String()) < 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(val.String()) < 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(val.String()) < 0 {
return SettingsErrInvalidPasswordMinSpecialCharCount()
}
}
return err
})
if err != nil {
return false, err
}
return true, nil
}

View File

@@ -379,19 +379,19 @@ type (
PasswordSecurity bool `kv:"-" json:"passwordSecurity"`
// The min password length
MinLength uint `kv:"min-length"`
MinLength uint `kv:"min-length" json:"minLength"`
// Minimum number of uppercase letters in password
MinUpperCase uint `kv:"min-upper-case"`
MinUpperCase uint `kv:"min-upper-case" json:"minUpperCase"`
// Minimum number of lowercase letters in password
MinLowerCase uint `kv:"min-lower-case"`
MinLowerCase uint `kv:"min-lower-case" json:"minLowerCase"`
// The min number of numeric characters
MinNumCount uint `kv:"min-num-count"`
MinNumCount uint `kv:"min-num-count" json:"minNumCount"`
// The min number of special characters
MinSpecialCount uint `kv:"min-special-count"`
MinSpecialCount uint `kv:"min-special-count" json:"minSpecialCount"`
}
)

View File

@@ -43,7 +43,7 @@ func TestSettingsList_noPermissions(t *testing.T) {
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to read settings")).
Assert(helpers.AssertError("settings.errors.notAllowedToRead")).
End()
}
@@ -84,10 +84,67 @@ func TestSettingsUpdate_noPermissions(t *testing.T) {
JSON(`{"values":[]}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to manage settings")).
Assert(helpers.AssertError("settings.errors.notAllowedToManage")).
End()
}
func TestSettingsUpdate_validation(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "settings.manage")
helpers.AllowMe(h, types.ComponentRbacResource(), "settings.read")
// Password constraints: The min password length should be 8
h.apiInit().
Patch("/settings/").
Header("Accept", "application/json").
JSON(`{"values":[{"name":"auth.internal.password-constraints.min-length","value":"7"}]}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("settings.errors.invalidPasswordMinLength")).
End()
// Password constraints: The min upper case count should not be a negative number
h.apiInit().
Patch("/settings/").
Header("Accept", "application/json").
JSON(`{"values":[{"name":"auth.internal.password-constraints.min-upper-case","value":"-1"}]}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("settings.errors.invalidPasswordMinUpperCase")).
End()
// Password constraints: The min lower case count should not be a negative number
h.apiInit().
Patch("/settings/").
Header("Accept", "application/json").
JSON(`{"values":[{"name":"auth.internal.password-constraints.min-lower-case","value":"-1"}]}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("settings.errors.invalidPasswordMinLowerCase")).
End()
// Password constraints: The min number of numeric characters should not be a negative number
h.apiInit().
Patch("/settings/").
Header("Accept", "application/json").
JSON(`{"values":[{"name":"auth.internal.password-constraints.min-num-count","value":"-1"}]}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("settings.errors.invalidPasswordMinNumCount")).
End()
// Password constraints: The min number of special characters should not be a negative number
h.apiInit().
Patch("/settings/").
Header("Accept", "application/json").
JSON(`{"values":[{"name":"auth.internal.password-constraints.min-special-count","value":"-1"}]}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("settings.errors.invalidPasswordMinSpecialCharCount")).
End()
}
func TestSettingsGet(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "settings.read")
@@ -125,7 +182,7 @@ func TestSettingsGet_noPermissions(t *testing.T) {
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to read settings")).
Assert(helpers.AssertError("settings.errors.notAllowedToRead")).
End()
}
@@ -138,7 +195,7 @@ func TestSettingsSet_noPermissions(t *testing.T) {
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to read settings")).
Assert(helpers.AssertError("settings.errors.notAllowedToRead")).
End()
}