3
0

Add resource limiting for users

This commit is contained in:
Denis Arh
2021-09-21 08:08:59 +02:00
parent 9f74d5c074
commit 1b3a811cfd
14 changed files with 238 additions and 15 deletions
+1
View File
@@ -366,6 +366,7 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) {
Template: app.Opt.Template,
Auth: app.Opt.Auth,
RBAC: app.Opt.RBAC,
Limit: app.Opt.Limit,
})
if err != nil {
+1 -1
View File
@@ -307,7 +307,7 @@ func makeMockAuthService() *mockAuthService {
opt: options.AuthOpt{},
}
serviceAuth := service.Auth()
serviceAuth := service.Auth(service.AuthOptions{})
svc := mockAuthService{
authService: serviceAuth,
+33
View File
@@ -0,0 +1,33 @@
package options
// 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:
// pkg/options/limit.yaml
type (
LimitOpt struct {
SystemUsers int `env:"LIMIT_SYSTEM_USERS"`
}
)
// Limit initializes and returns a LimitOpt with default values
func Limit() (o *LimitOpt) {
o = &LimitOpt{}
fill(o)
// Function that allows access to custom logic inside the parent function.
// The custom logic in the other file should be like:
// func (o *Limit) Defaults() {...}
func(o interface{}) {
if def, ok := o.(interface{ Defaults() }); ok {
def.Defaults()
}
}(o)
return
}
+50
View File
@@ -0,0 +1,50 @@
docs:
title: Limits
props:
- name: systemUsers
type: int
description: |-
Maximum number of valid (not deleted, not suspended) users
# @todo to be implemented
# - name: systemRoles
# type: int
# description: |-
# Maximum number of valid (not deleted, not archived) roles
#
# @todo to be implemented
# - name: systemGatewayRouts
# type: int
# description: |-
# Maximum number of valid (not deleted) gateway routes
#
# @todo to be implemented
# - name: systemTemplates
# type: int
# description: |-
# Maximum number of valid (not deleted) templates
#
# @todo to be implemented
# - name: composeNamespaces
# type: int
# description: |-
# Maximum number of valid (not deleted) compose namespaces
#
# @todo to be implemented
# - name: composeModules
# type: int
# description: |-
# Maximum number of valid (not deleted) compose modules accross all namespaces
#
# @todo to be implemented
# - name: composeRecords
# type: int
# description: |-
# Maximum number of valid (not deleted) compose records accross all namespaces and modules
#
# @todo to be implemented
# - name: automationWorkflows
# type: int
# description: |-
# Maximum number of valid (not deleted) automation workflows
+2
View File
@@ -25,6 +25,7 @@ type (
Workflow WorkflowOpt
RBAC RBACOpt
Locale LocaleOpt
Limit LimitOpt
}
)
@@ -53,5 +54,6 @@ func Init() *Options {
Workflow: *Workflow(),
RBAC: *RBAC(),
Locale: *Locale(),
Limit: *Limit(),
}
}
+1 -1
View File
@@ -95,7 +95,7 @@ func Users(ctx context.Context, app serviceInitializer) *cobra.Command {
ctx = auth.SetIdentityToContext(ctx, auth.ServiceUser())
var (
authSvc = service.Auth()
authSvc = service.Auth(service.AuthOptions{})
// @todo email validation
user = &types.User{Email: args[0]}
+32 -2
View File
@@ -33,9 +33,15 @@ type (
settings *types.AppSettings
notifications AuthNotificationService
opt AuthOptions
providerValidator func(string) error
}
AuthOptions struct {
LimitUsers int
}
authAccessController interface {
CanImpersonateUser(context.Context, *types.User) bool
CanUpdateUser(context.Context, *types.User) bool
@@ -69,7 +75,7 @@ func defaultProviderValidator(provider string) error {
}
}
func Auth() *auth {
func Auth(opt AuthOptions) *auth {
return &auth{
eventbus: eventbus.Service(),
ac: DefaultAccessControl,
@@ -80,6 +86,8 @@ func Auth() *auth {
store: DefaultStore,
providerValidator: defaultProviderValidator,
opt: opt,
}
}
@@ -220,6 +228,10 @@ func (svc auth) External(ctx context.Context, profile types.ExternalAuthUser) (u
return err
}
if err = svc.checkLimits(ctx); err != nil {
return err
}
u.ID = nextID()
u.CreatedAt = *now()
if err = store.CreateUser(ctx, svc.store, u); err != nil {
@@ -378,6 +390,10 @@ func (svc auth) InternalSignUp(ctx context.Context, input *types.User, password
return err
}
if err = svc.checkLimits(ctx); err != nil {
return err
}
// Whitelisted user data to copy
err = store.CreateUser(ctx, svc.store, nUser)
if err != nil {
@@ -1060,7 +1076,7 @@ func (svc auth) autoPromote(ctx context.Context, u *types.User) (err error) {
aam = &authActionProps{user: u, role: &types.Role{}}
)
if c, err = store.CountUsers(ctx, svc.store, types.UserFilter{Kind: types.NormalUser}); err != nil {
if c, err = countValidUsers(ctx, svc.store); err != nil {
return err
}
@@ -1428,3 +1444,17 @@ func (svc auth) LoadRoleMemberships(ctx context.Context, u *types.User) error {
func (svc auth) GetProviders() types.ExternalAuthProviderSet {
return CurrentSettings.Auth.External.Providers
}
func (svc auth) checkLimits(ctx context.Context) error {
if svc.opt.LimitUsers == 0 {
return nil
}
if c, err := countValidUsers(ctx, svc.store); err != nil {
return err
} else if c >= uint(svc.opt.LimitUsers) {
return AuthErrMaxUserLimitReached()
}
return nil
}
+34
View File
@@ -1728,6 +1728,40 @@ func AuthErrInvalidEmailOTP(mm ...*authActionProps) *errors.Error {
return e
}
// AuthErrMaxUserLimitReached returns "system:auth.maxUserLimitReached" as *errors.Error
//
//
// This function is auto-generated.
//
func AuthErrMaxUserLimitReached(mm ...*authActionProps) *errors.Error {
var p = &authActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("you have reached your user limit, contact your Corteza administrator", nil),
errors.Meta("type", "maxUserLimitReached"),
errors.Meta("resource", "system:auth"),
errors.Meta(authPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "auth.errors.maxUserLimitReached"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
+4
View File
@@ -200,3 +200,7 @@ errors:
- error: invalidEmailOTP
message: "invalid code"
severity: warning
- error: maxUserLimitReached
message: "you have reached your user limit, contact your Corteza administrator"
severity: warning
+3 -2
View File
@@ -33,6 +33,7 @@ type (
Template options.TemplateOpt
Auth options.AuthOpt
RBAC options.RBACOpt
Limit options.LimitOpt
}
eventDispatcher interface {
@@ -162,9 +163,9 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websock
DefaultRenderer = Renderer(c.Template)
DefaultReport = Report(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service())
DefaultAuthNotification = AuthNotification(CurrentSettings, DefaultRenderer, c.Auth)
DefaultAuth = Auth()
DefaultAuth = Auth(AuthOptions{LimitUsers: c.Limit.SystemUsers})
DefaultAuthClient = AuthClient(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service(), c.Auth)
DefaultUser = User()
DefaultUser = User(UserOptions{LimitUsers: c.Limit.SystemUsers})
DefaultRole = Role()
DefaultApplication = Application(DefaultStore, DefaultAccessControl, DefaultActionlog, eventbus.Service())
DefaultReminder = Reminder(ctx, DefaultLogger.Named("reminder"), ws)
+39 -1
View File
@@ -38,12 +38,18 @@ type (
store store.Storer
opt UserOptions
// List (cache) of preloaded users, accessible by handle
//
// It also does negative caching by assigning empty User structs
preloaded map[string]*types.User
}
UserOptions struct {
LimitUsers int
}
userAuth interface {
CheckPasswordStrength(string) bool
SetPasswordCredentials(context.Context, uint64, string) error
@@ -89,7 +95,7 @@ type (
}
)
func User() *user {
func User(opt UserOptions) *user {
return &user{
eventbus: eventbus.Service(),
ac: DefaultAccessControl,
@@ -100,6 +106,8 @@ func User() *user {
actionlog: DefaultActionlog,
opt: opt,
preloaded: make(map[string]*types.User),
}
}
@@ -362,6 +370,10 @@ func (svc user) Create(ctx context.Context, new *types.User) (u *types.User, err
return UserErrInvalidEmail()
}
if err = svc.checkLimits(ctx); err != nil {
return err
}
if err = svc.eventbus.WaitFor(ctx, event.UserBeforeCreate(new, u)); err != nil {
return
}
@@ -577,6 +589,10 @@ func (svc user) Undelete(ctx context.Context, userID uint64) (err error) {
return UserErrNotAllowedToDelete()
}
if err = svc.checkLimits(ctx); err != nil {
return err
}
if !svc.ac.CanDeleteUser(ctx, u) {
return UserErrNotAllowedToDelete()
}
@@ -663,6 +679,10 @@ func (svc user) Unsuspend(ctx context.Context, userID uint64) (err error) {
return UserErrNotAllowedToUnsuspend()
}
if err = svc.checkLimits(ctx); err != nil {
return err
}
u.SuspendedAt = nil
if err = store.UpdateUser(ctx, svc.store, u); err != nil {
return
@@ -795,6 +815,24 @@ func (svc user) DeleteAuthSessionsByUserID(ctx context.Context, userID uint64) (
return svc.recordAction(ctx, uaProps, UserActionDeleteAuthSessions, err)
}
func (svc user) checkLimits(ctx context.Context) error {
if svc.opt.LimitUsers == 0 {
return nil
}
if c, err := countValidUsers(ctx, svc.store); err != nil {
return err
} else if c >= uint(svc.opt.LimitUsers) {
return UserErrMaxUserLimitReached()
}
return nil
}
func countValidUsers(ctx context.Context, s store.Users) (c uint, err error) {
return store.CountUsers(ctx, s, types.UserFilter{Kind: types.NormalUser})
}
// UniqueCheck verifies user's email, username and handle
func uniqueUserCheck(ctx context.Context, s store.Storer, u *types.User) (err error) {
isUnique := func(field string) bool {
+34 -5
View File
@@ -1266,11 +1266,6 @@ func UserErrPasswordNotSecure(mm ...*userActionProps) *errors.Error {
errors.Meta("type", "passwordNotSecure"),
errors.Meta("resource", "system:user"),
// link to documentation; formatting applies in case we need some special link formatting
errors.Meta("documentation", p.Format("foo", nil)),
// details, used in detailed eror reporting
errors.Meta("details", p.Format("foo bar", nil)),
errors.Meta(userPropsMetaKey{}, p),
// translation namespace & key
@@ -1286,6 +1281,40 @@ func UserErrPasswordNotSecure(mm ...*userActionProps) *errors.Error {
return e
}
// UserErrMaxUserLimitReached returns "system:user.maxUserLimitReached" as *errors.Error
//
//
// This function is auto-generated.
//
func UserErrMaxUserLimitReached(mm ...*userActionProps) *errors.Error {
var p = &userActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("you have reached your user limit, contact your Corteza administrator", nil),
errors.Meta("type", "maxUserLimitReached"),
errors.Meta("resource", "system:user"),
errors.Meta(userPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "system"),
errors.Meta(locale.ErrorMetaKey{}, "user.errors.maxUserLimitReached"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
+3 -2
View File
@@ -146,6 +146,7 @@ errors:
- error: passwordNotSecure
message: "provided password is not secure; use longer password with more non-alphanumeric character"
documentation: foo
details: foo bar
- error: maxUserLimitReached
message: "you have reached your user limit, contact your Corteza administrator"
severity: warning
+1 -1
View File
@@ -177,7 +177,7 @@ func TestScimUserPassword(t *testing.T) {
h.clearUsers()
service.CurrentSettings.Auth.Internal.Enabled = true
auth := service.Auth()
auth := service.Auth(service.AuthOptions{})
h.scimApiInit().
Post("/Users").