Add resource limiting for users
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -307,7 +307,7 @@ func makeMockAuthService() *mockAuthService {
|
||||
opt: options.AuthOpt{},
|
||||
}
|
||||
|
||||
serviceAuth := service.Auth()
|
||||
serviceAuth := service.Auth(service.AuthOptions{})
|
||||
|
||||
svc := mockAuthService{
|
||||
authService: serviceAuth,
|
||||
|
||||
Generated
+33
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Generated
+34
@@ -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
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Generated
+34
-5
@@ -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
|
||||
}
|
||||
|
||||
// *********************************************************************************************************************
|
||||
// *********************************************************************************************************************
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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").
|
||||
|
||||
Reference in New Issue
Block a user