Skip to content

Commit

Permalink
Add resource limiting for users
Browse files Browse the repository at this point in the history
  • Loading branch information
darh committed Sep 22, 2021
1 parent 9f74d5c commit 1b3a811
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 15 deletions.
1 change: 1 addition & 0 deletions app/boot_levels.go
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion auth/handlers/mock_test.go
Expand Up @@ -307,7 +307,7 @@ func makeMockAuthService() *mockAuthService {
opt: options.AuthOpt{},
}

serviceAuth := service.Auth()
serviceAuth := service.Auth(service.AuthOptions{})

svc := mockAuthService{
authService: serviceAuth,
Expand Down
33 changes: 33 additions & 0 deletions pkg/options/limit.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions pkg/options/limit.yaml
@@ -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 changes: 2 additions & 0 deletions pkg/options/options.go
Expand Up @@ -25,6 +25,7 @@ type (
Workflow WorkflowOpt
RBAC RBACOpt
Locale LocaleOpt
Limit LimitOpt
}
)

Expand Down Expand Up @@ -53,5 +54,6 @@ func Init() *Options {
Workflow: *Workflow(),
RBAC: *RBAC(),
Locale: *Locale(),
Limit: *Limit(),
}
}
2 changes: 1 addition & 1 deletion system/commands/users.go
Expand Up @@ -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]}
Expand Down
34 changes: 32 additions & 2 deletions system/service/auth.go
Expand Up @@ -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
Expand Down Expand Up @@ -69,7 +75,7 @@ func defaultProviderValidator(provider string) error {
}
}

func Auth() *auth {
func Auth(opt AuthOptions) *auth {
return &auth{
eventbus: eventbus.Service(),
ac: DefaultAccessControl,
Expand All @@ -80,6 +86,8 @@ func Auth() *auth {
store: DefaultStore,

providerValidator: defaultProviderValidator,

opt: opt,
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 changes: 34 additions & 0 deletions system/service/auth_actions.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions system/service/auth_actions.yaml
Expand Up @@ -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
5 changes: 3 additions & 2 deletions system/service/service.go
Expand Up @@ -33,6 +33,7 @@ type (
Template options.TemplateOpt
Auth options.AuthOpt
RBAC options.RBACOpt
Limit options.LimitOpt
}

eventDispatcher interface {
Expand Down Expand Up @@ -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)
Expand Down
40 changes: 39 additions & 1 deletion system/service/user.go
Expand Up @@ -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
Expand Down Expand Up @@ -89,7 +95,7 @@ type (
}
)

func User() *user {
func User(opt UserOptions) *user {
return &user{
eventbus: eventbus.Service(),
ac: DefaultAccessControl,
Expand All @@ -100,6 +106,8 @@ func User() *user {

actionlog: DefaultActionlog,

opt: opt,

preloaded: make(map[string]*types.User),
}
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 1b3a811

Please sign in to comment.