diff --git a/codegen/v2/actionlog/actions.gen.go.tpl b/codegen/v2/actionlog/actions.gen.go.tpl index a96b34e3d..3de985db7 100644 --- a/codegen/v2/actionlog/actions.gen.go.tpl +++ b/codegen/v2/actionlog/actions.gen.go.tpl @@ -347,63 +347,77 @@ func {{ camelCase "" $.Service "Err" $e.Error }}(props ... *{{ $.Service }}Actio // This function is auto-generated. // func (svc {{ $.Service }}) recordAction(ctx context.Context, props *{{ $.Service }}ActionProps, action func(... *{{ $.Service }}ActionProps) *{{ $.Service }}Action, err error) error { - // If non-{{ $.Service }}Error error was passed, - // wrap it with generic error var ( - svcErr *{{ $.Service }}Error - ok bool - ) + ok bool + + // Return error + retError *{{ $.Service }}Error + + // Recorder error + recError *{{ $.Service }}Error + ) if err != nil { - if svcErr, ok = err.(*{{ $.Service }}Error); !ok { - // wrap non-{{ $.Service }} errors - svcErr = {{ camelCase "" $.Service "err" "generic" }}(props).Wrap(err) + if retError, ok = err.(*{{ $.Service }}Error); !ok { + // got non-{{ $.Service }} error, wrap it with {{ camelCase "" $.Service "err" "generic" }} + retError = {{ camelCase "" $.Service "err" "generic" }}(props).Wrap(err) - // make sure we return generic error! - err = svcErr - } else { - // find the original cause for this error - // for the purpose of logging - // - // we'll return the received error as-is + // copy action to returning and recording error + retError.action = action().action + + // we'll use {{ camelCase "" $.Service "err" "generic" }} for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + // copy action to returning and recording error + retError.action = action().action + // start with copy of return error for recording + // this will be updated with tha root cause as we try and + // unwrap the error + recError = retError + + // find the original recError for this error + // for the purpose of logging + var unwrappedError error = retError for { - ue := errors.Unwrap(svcErr) - if ue == nil { - // nothing wrapped + if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil { + // nothing wrapped break } - if _, ok = ue.(*{{ $.Service }}Error); !ok { - // wrapped error is not of type *{{ $.Service }}Error - break + // update recError ONLY of wrapped error is of type {{ $.Service }}Error + if unwrappedSinkError, ok := unwrappedError.(*{{ $.Service }}Error); ok { + recError = unwrappedSinkError } - - svcErr = ue.(*{{ $.Service }}Error) } - if svcErr.props == nil { - // got {{ $.Service }}Error w/o props, - // assign props from args - svcErr.props = props - } - } + if retError.props == nil { + // set props on returning error if empty + retError.props = props + } - if action != nil { - // copy action to error - a := action() - svcErr.action = a.action + if recError.props == nil { + // set props on recording error if empty + recError.props = props + } } } if svc.actionlog != nil { - if svcErr != nil { - // failed action, log error - svc.actionlog.Record(ctx, svcErr) + if retError != nil { + // failed action, log error + svc.actionlog.Record(ctx, recError) } else if action != nil { - // successful + // successful svc.actionlog.Record(ctx, action(props)) } } - return err + if err == nil { + // retError not an interface and that WILL (!!) cause issues + // with nil check (== nil) when it is not explicitly returned + return nil + } + + return retError } diff --git a/pkg/permissions/permissions.go b/pkg/permissions/permissions.go index a41dc0935..494e2ac75 100644 --- a/pkg/permissions/permissions.go +++ b/pkg/permissions/permissions.go @@ -25,10 +25,10 @@ type ( const ( // Hardcoded Role ID for everyone - EveryoneRoleID = 1 + EveryoneRoleID uint64 = 1 // Hardcoded ID for Admin role - AdminsRoleID = 2 + AdminsRoleID uint64 = 2 ) func (op Operation) String() string { diff --git a/system/service/access_control.go b/system/service/access_control.go index 7f550bba2..b5c0b6524 100644 --- a/system/service/access_control.go +++ b/system/service/access_control.go @@ -2,6 +2,8 @@ package service import ( "context" + + "github.com/cortezaproject/corteza-server/pkg/actionlog" internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/permissions" @@ -11,6 +13,7 @@ import ( type ( accessControl struct { permissions accessControlPermissionServicer + actionlog actionlog.Recorder } accessControlPermissionServicer interface { @@ -28,6 +31,7 @@ type ( func AccessControl(perm accessControlPermissionServicer) *accessControl { return &accessControl{ permissions: perm, + actionlog: DefaultActionlog, } } @@ -181,15 +185,35 @@ func (svc accessControl) can(ctx context.Context, res permissionResource, op per func (svc accessControl) Grant(ctx context.Context, rr ...*permissions.Rule) error { if !svc.CanGrant(ctx) { - return ErrNoGrantPermissions + return AccessControlErrNotAllowedToSetPermissions() } - return svc.permissions.Grant(ctx, svc.Whitelist(), rr...) + if err := svc.permissions.Grant(ctx, svc.Whitelist(), rr...); err != nil { + return AccessControlErrGeneric().Wrap(err) + } + + svc.logGrants(ctx, rr) + + return nil +} + +func (svc accessControl) logGrants(ctx context.Context, rr []*permissions.Rule) { + if svc.actionlog == nil { + return + } + + for _, r := range rr { + g := AccessControlActionGrant(&accessControlActionProps{r}) + g.log = r.String() + g.resource = r.Resource.String() + + svc.actionlog.Record(ctx, g) + } } func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) (permissions.RuleSet, error) { if !svc.CanGrant(ctx) { - return nil, ErrNoPermissions + return nil, AccessControlErrNotAllowedToSetPermissions() } return svc.permissions.FindRulesByRoleID(roleID), nil diff --git a/system/service/access_control_actions.gen.go b/system/service/access_control_actions.gen.go new file mode 100644 index 000000000..9f1a8baea --- /dev/null +++ b/system/service/access_control_actions.gen.go @@ -0,0 +1,407 @@ +package service + +// This file is auto-generated from system/service/access_control_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/pkg/permissions" +) + +type ( + accessControlActionProps struct { + rule *permissions.Rule + } + + accessControlAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *accessControlActionProps + } + + accessControlError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *accessControlActionProps + } +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setRule updates accessControlActionProps's rule +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *accessControlActionProps) setRule(rule *permissions.Rule) *accessControlActionProps { + p.rule = rule + return p +} + +// serialize converts accessControlActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p accessControlActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + str = func(i interface{}) string { return fmt.Sprintf("%v", i) } + ) + + if p.rule != nil { + m["rule.operation"] = str(p.rule.Operation) + m["rule.roleID"] = str(p.rule.RoleID) + m["rule.access"] = str(p.rule.Access) + m["rule.resource"] = str(p.rule.Resource) + } + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p accessControlActionProps) tr(in string, err error) string { + var pairs = []string{"{err}"} + + if err != nil { + for { + // Unwrap errors + ue := errors.Unwrap(err) + if ue == nil { + break + } + + err = ue + } + + pairs = append(pairs, err.Error()) + } else { + pairs = append(pairs, "nil") + } + + if p.rule != nil { + pairs = append(pairs, "{rule}", fmt.Sprintf("%v", p.rule.Operation)) + pairs = append(pairs, "{rule.operation}", fmt.Sprintf("%v", p.rule.Operation)) + pairs = append(pairs, "{rule.roleID}", fmt.Sprintf("%v", p.rule.RoleID)) + pairs = append(pairs, "{rule.access}", fmt.Sprintf("%v", p.rule.Access)) + pairs = append(pairs, "{rule.resource}", fmt.Sprintf("%v", p.rule.Resource)) + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *accessControlAction) String() string { + var props = &accessControlActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *accessControlAction) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error methods + +// String returns loggable description as string +// +// It falls back to message if log is not set +// +// This function is auto-generated. +// +func (e *accessControlError) String() string { + var props = &accessControlActionProps{} + + if e.props != nil { + props = e.props + } + + if e.wrap != nil && !strings.Contains(e.log, "{err}") { + // Suffix error log with {err} to ensure + // we log the cause for this error + e.log += ": {err}" + } + + return props.tr(e.log, e.wrap) +} + +// Error satisfies +// +// This function is auto-generated. +// +func (e *accessControlError) Error() string { + var props = &accessControlActionProps{} + + if e.props != nil { + props = e.props + } + + return props.tr(e.message, e.wrap) +} + +// Is fn for error equality check +// +// This function is auto-generated. +// +func (e *accessControlError) Is(Resource error) bool { + t, ok := Resource.(*accessControlError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps accessControlError around another error +// +// This function is auto-generated. +// +func (e *accessControlError) Wrap(err error) *accessControlError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *accessControlError) Unwrap() error { + return e.wrap +} + +func (e *accessControlError) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Error: e.Error(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action constructors + +// AccessControlActionGrant returns "system:access_control.grant" error +// +// This function is auto-generated. +// +func AccessControlActionGrant(props ...*accessControlActionProps) *accessControlAction { + a := &accessControlAction{ + timestamp: time.Now(), + resource: "system:access_control", + action: "grant", + log: "grant", + severity: actionlog.Error, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// AccessControlErrGeneric returns "system:access_control.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func AccessControlErrGeneric(props ...*accessControlActionProps) *accessControlError { + var e = &accessControlError{ + timestamp: time.Now(), + resource: "system:access_control", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *accessControlActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AccessControlErrNotAllowedToSetPermissions returns "system:access_control.notAllowedToSetPermissions" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AccessControlErrNotAllowedToSetPermissions(props ...*accessControlActionProps) *accessControlError { + var e = &accessControlError{ + timestamp: time.Now(), + resource: "system:access_control", + error: "notAllowedToSetPermissions", + action: "error", + message: "not allowed to set permissions", + log: "not allowed to set permissions", + severity: actionlog.Alert, + props: func() *accessControlActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* + +// recordAction is a service helper function wraps function that can return error +// +// context is used to enrich audit log entry with current user info, request ID, IP address... +// props are collected action/error properties +// action (optional) fn will be used to construct accessControlAction struct from given props (and error) +// err is any error that occurred while action was happening +// +// Action has success and fail (error) state: +// - when recorded without an error (4th param), action is recorded as successful. +// - when an additional error is given (4th param), action is used to wrap +// the additional error +// +// This function is auto-generated. +// +func (svc accessControl) recordAction(ctx context.Context, props *accessControlActionProps, action func(...*accessControlActionProps) *accessControlAction, err error) error { + var ( + ok bool + + // Return error + retError *accessControlError + + // Recorder error + recError *accessControlError + ) + + if err != nil { + if retError, ok = err.(*accessControlError); !ok { + // got non-accessControl error, wrap it with AccessControlErrGeneric + retError = AccessControlErrGeneric(props).Wrap(err) + + // copy action to returning and recording error + retError.action = action().action + + // we'll use AccessControlErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + // copy action to returning and recording error + retError.action = action().action + // start with copy of return error for recording + // this will be updated with tha root cause as we try and + // unwrap the error + recError = retError + + // find the original recError for this error + // for the purpose of logging + var unwrappedError error = retError + for { + if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil { + // nothing wrapped + break + } + + // update recError ONLY of wrapped error is of type accessControlError + if unwrappedSinkError, ok := unwrappedError.(*accessControlError); ok { + recError = unwrappedSinkError + } + } + + if retError.props == nil { + // set props on returning error if empty + retError.props = props + } + + if recError.props == nil { + // set props on recording error if empty + recError.props = props + } + } + } + + if svc.actionlog != nil { + if retError != nil { + // failed action, log error + svc.actionlog.Record(ctx, recError) + } else if action != nil { + // successful + svc.actionlog.Record(ctx, action(props)) + } + } + + if err == nil { + // retError not an interface and that WILL (!!) cause issues + // with nil check (== nil) when it is not explicitly returned + return nil + } + + return retError +} diff --git a/system/service/access_control_actions.yaml b/system/service/access_control_actions.yaml new file mode 100644 index 000000000..e83c17a91 --- /dev/null +++ b/system/service/access_control_actions.yaml @@ -0,0 +1,25 @@ +# List of security/audit events and errors that we need to log + +resource: system:access_control +service: accessControl + +# Default sensitivity for actions +defaultActionSeverity: note + +# default severity for errors +defaultErrorSeverity: alert + +import: + - github.com/cortezaproject/corteza-server/pkg/permissions + +props: + - name: rule + type: "*permissions.Rule" + fields: [ operation, roleID, access, resource ] + +actions: + - action: grant + +errors: + - error: notAllowedToSetPermissions + message: "not allowed to set permissions" diff --git a/system/service/auth.go b/system/service/auth.go index c41000405..ad03a58cc 100644 --- a/system/service/auth.go +++ b/system/service/auth.go @@ -8,14 +8,12 @@ import ( "time" "github.com/markbates/goth" - "github.com/pkg/errors" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" "golang.org/x/crypto/bcrypt" + "github.com/cortezaproject/corteza-server/pkg/actionlog" + internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/handle" - "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/permissions" "github.com/cortezaproject/corteza-server/pkg/rand" "github.com/cortezaproject/corteza-server/system/repository" @@ -25,11 +23,11 @@ import ( type ( auth struct { - db db - ctx context.Context - logger *zap.Logger + db db + ctx context.Context - eventbus eventDispatcher + actionlog actionlog.Recorder + eventbus eventDispatcher subscription authSubscriptionChecker credentials repository.CredentialsRepository @@ -50,8 +48,8 @@ type ( InternalSignUp(input *types.User, password string) (*types.User, error) InternalLogin(email string, password string) (*types.User, error) - SetPassword(userID uint64, newPassword string) error - ChangePassword(userID uint64, oldPassword, newPassword string) error + SetPassword(userID uint64, AuthActionPassword string) error + ChangePassword(userID uint64, oldPassword, AuthActionPassword string) error IssueAuthRequestToken(user *types.User) (token string, err error) ValidateAuthRequestToken(token string) (user *types.User, err error) @@ -65,7 +63,7 @@ type ( LoadRoleMemberships(*types.User) error - checkPasswordStrength(string) error + checkPasswordStrength(string) bool changePassword(uint64, string) error } @@ -95,13 +93,13 @@ func defaultProviderValidator(provider string) error { func Auth(ctx context.Context) AuthService { return (&auth{ - logger: DefaultLogger.Named("auth"), - eventbus: eventbus.Service(), subscription: CurrentSubscription, settings: CurrentSettings, notifications: DefaultAuthNotification, + actionlog: DefaultActionlog, + providerValidator: defaultProviderValidator, now: func() *time.Time { @@ -111,12 +109,13 @@ func Auth(ctx context.Context) AuthService { }).With(ctx) } +// With returns copy of service with new context +// obsolete approach, will be removed ASAP func (svc auth) With(ctx context.Context) AuthService { db := repository.DB(ctx) return &auth{ - db: db, - ctx: ctx, - logger: logger.AddRequestID(ctx, svc.logger), + db: db, + ctx: ctx, credentials: repository.Credentials(ctx, db), users: repository.User(ctx, db), @@ -128,15 +127,12 @@ func (svc auth) With(ctx context.Context) AuthService { eventbus: svc.eventbus, providerValidator: svc.providerValidator, + actionlog: svc.actionlog, + now: svc.now, } } -// log() returns zap's logger with requestID from current context and fields. -func (svc auth) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(ctx, svc.logger).With(fields...) -} - // External func performs login/signup procedures // // We fully trust external auth sources (see system/auth/external) to provide a valid & validates @@ -153,28 +149,29 @@ func (svc auth) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { // 2.3. create credentials for that social login // func (svc auth) External(profile goth.User) (u *types.User, err error) { - if !svc.settings.Auth.External.Enabled { - return nil, errors.New("external authentication disabled") - } - - if err = svc.providerValidator(profile.Provider); err != nil { - return nil, err - } - - if profile.Email == "" { - return nil, errors.New("can not use profile data without an email") - } - var ( - log = svc.log(svc.ctx, zap.String("provider", profile.Provider)) + authProvider = &types.AuthProvider{Provider: profile.Provider} - authProvider = &types.AuthProvider{ - Provider: profile.Provider, + aam = &authActionProps{ + email: profile.Email, + provider: profile.Provider, + user: u, } ) - return u, svc.db.Transaction(func() error { - var c *types.Credentials + err = svc.db.Transaction(func() error { + if !svc.settings.Auth.External.Enabled { + return AuthErrExternalDisabledByConfig(aam) + } + + if err = svc.providerValidator(profile.Provider); err != nil { + return err + } + + if !reEmail.MatchString(profile.Email) { + return AuthErrProfileWithoutValidEmail(aam) + } + if cc, err := svc.credentials.FindByCredentials(profile.Provider, profile.UserID); err == nil { // Credentials found, load user for _, c := range cc { @@ -182,12 +179,15 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) { continue } + // Add credentials ID for audit log + aam.setCredentials(c) + if u, err = svc.users.FindByID(c.OwnerID); err != nil { if repository.ErrUserNotFound.Eq(err) { // Orphaned credentials (no owner) // try to auto-fix this by removing credentials and recreating user - if err := svc.credentials.DeleteByID(c.ID); err != nil { - return errors.Wrap(err, "could not cleanup orphaned credentials") + if err = svc.credentials.DeleteByID(c.ID); err != nil { + return err } else { goto findByEmail } @@ -195,6 +195,10 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) { return err } + // Add user ID for audit log + aam.setUser(u) + svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u) + if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(u, authProvider)); err != nil { return err } @@ -206,30 +210,35 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) { return err } - log.Info("updating credentials entry for existing user", - zap.Uint64("credentialsID", c.ID), - zap.Uint64("userID", u.ID), - zap.String("email", u.Email), - ) - defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterLogin(u, authProvider)) - - return nil + return svc.recordAction(svc.ctx, aam, AuthActionUpdateCredentials, nil) } else { // Scenario: linked to an invalid user - u = nil - continue + if len(cc) > 1 { + // try with next credentials + u = nil + continue + } + + return AuthErrCredentialsLinkedToInvalidUser(aam) } } // If we could not find anything useful, // we can search for user via email + // (using goto for consistency) + goto findByEmail } else { // A serious error occurred, bail out... return err } findByEmail: + // Reset audit meta data that might got set during credentials check + aam.setEmail(profile.Email). + setCredentials(nil). + setUser(nil) + // Find user via his email if u, err = svc.users.FindByEmail(profile.Email); repository.ErrUserNotFound.Eq(err) { // @todo check if it is ok to auto-create a user here @@ -246,37 +255,38 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) { } if err = svc.CanRegister(); err != nil { - return err + return AuthErrSubscription(aam).Wrap(err) } if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeSignup(u, authProvider)); err != nil { return err } + if u.Handle == "" { createHandle(svc.users, u) } if u, err = svc.users.Create(u); err != nil { - return errors.Wrap(err, "could not create user after successful external authentication") + return err } - log.Info("created new user after successful social auth", - zap.Uint64("userID", u.ID), - zap.String("email", u.Email), - ) + aam.setUser(nil) + svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u) defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterSignup(u, authProvider)) - _ = svc.autoPromote(u) + svc.recordAction(svc.ctx, aam, AuthActionExternalSignup, nil) + + // Auto-promote first user + if err = svc.autoPromote(u); err != nil { + return err + } } else if err != nil { return err - } else if !u.Valid() { - return errors.Errorf("user not valid") } else { - log.Info("existing user authenticated", - zap.Uint64("userID", u.ID), - zap.String("email", u.Email), - ) + // User found + aam.setUser(u) + svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u) if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(u, authProvider)); err != nil { return err @@ -284,9 +294,15 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) { defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterLogin(u, authProvider)) + // If user + if !u.Valid() { + return AuthErrFailedForDisabledUser(aam).Wrap(err) + } } - c = &types.Credentials{ + // If we got to this point, assume that user is authenticated + // but credentials need to be stored + c := &types.Credentials{ Kind: profile.Provider, OwnerID: u.ID, Credentials: profile.UserID, @@ -297,15 +313,14 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) { return err } - log.Info("new credentials created for existing user", - zap.Uint64("credentialsID", c.ID), - zap.Uint64("userID", u.ID), - zap.String("email", u.Email), - ) + aam.setCredentials(c) + svc.recordAction(svc.ctx, aam, AuthActionCreateCredentials, nil) // Owner loaded, carry on. return nil }) + + return u, svc.recordAction(svc.ctx, aam, AuthActionAuthenticate, err) } // FrontendRedirectURL - a proxy to frontend redirect url setting @@ -319,331 +334,334 @@ func (svc auth) FrontendRedirectURL() string { // // We're accepting the whole user object here and copy all we need to the new user func (svc auth) InternalSignUp(input *types.User, password string) (u *types.User, err error) { - if !svc.settings.Auth.Internal.Enabled { - return nil, errors.New("internal authentication disabled") - } + var ( + authProvider = &types.AuthProvider{Provider: credentialsTypePassword} - if !svc.settings.Auth.Internal.Signup.Enabled { - return nil, errors.New("internal signup disabled") - } + aam = &authActionProps{ + email: input.Email, + credentials: &types.Credentials{Kind: credentialsTypePassword}, + user: u, + } + ) - if input == nil { - return nil, errors.New("invalid signup input") - } + err = func() error { + if !svc.settings.Auth.Internal.Enabled || !svc.settings.Auth.Internal.Signup.Enabled { + return AuthErrInternalSignupDisabledByConfig(aam) + } - if err = svc.validateInternalSignUp(input.Email); err != nil { - return - } + if input == nil || !reEmail.MatchString(input.Email) { + return AuthErrInvalidEmailFormat(aam) + } - if !handle.IsValid(input.Handle) { - return nil, ErrInvalidHandle.withStack() - } - - existing, err := svc.users.FindByEmail(input.Email) - - if err == nil && existing.Valid() { if len(password) == 0 { - return nil, errors.New("invalid username/password combination") + return AuthErrPasswordNotSecure(aam) } - cc, err := svc.credentials.FindByKind(existing.ID, credentialsTypePassword) - if err != nil { - return nil, errors.Wrap(err, "could not find credentials") + if !handle.IsValid(input.Handle) { + return AuthErrInvalidHandle(aam) } - err = svc.checkPassword(password, cc) - if err != nil { - return nil, errors.Wrap(err, "user with this email already exists") - } - - // We're not actually doing sign-up here - user exists, - // password is a match, so lets trigger before/after user login events - if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(existing, &types.AuthProvider{})); err != nil { - return nil, err - } - - if !existing.EmailConfirmed { - err = svc.sendEmailAddressConfirmationToken(existing) + var eUser *types.User + eUser, err = svc.users.FindByEmail(input.Email) + if err == nil && eUser.Valid() { + var cc types.CredentialsSet + cc, err = svc.credentials.FindByKind(eUser.ID, credentialsTypePassword) if err != nil { - return nil, err + return err + } + + if !svc.checkPassword(password, cc) { + return AuthErrInvalidCredentials(aam) + } + + // We're not actually doing sign-up here - user exists, + // password is a match, so lets trigger before/after user login events + if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(eUser, authProvider)); err != nil { + return err + } + + if !eUser.EmailConfirmed { + err = svc.sendEmailAddressConfirmationToken(eUser) + if err != nil { + return err + } + } + + defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterLogin(eUser, authProvider)) + u = eUser + return nil + + // if !svc.settings.internalSignUpSendEmailOnExisting { + // return nil,errors.Wrap(err, "user with this email already exists") + // } + + // User already exists, but we're nice and we'll send this user an + // email that will help him to login + // if !u.Valid() { + // return nil,errors.New("could not validate the user") + // } + // + // return nil,nil + } else if !repository.ErrUserNotFound.Eq(err) { + return err + } + + if err = svc.CanRegister(); err != nil { + return err + } + + var nUser = &types.User{ + Email: input.Email, + Name: input.Name, + Username: input.Username, + Handle: input.Handle, + + // Do we need confirmed email? + EmailConfirmed: !svc.settings.Auth.Internal.Signup.EmailConfirmationRequired, + } + + if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeSignup(nUser, authProvider)); err != nil { + return err + } + + if input.Handle == "" { + createHandle(svc.users, input) + } + + // Whitelisted user data to copy + u, err = svc.users.Create(nUser) + + if err != nil { + return err + } + + aam.setUser(u) + defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterSignup(u, authProvider)) + + if err = svc.autoPromote(u); err != nil { + return err + } + + if len(password) > 0 { + err = svc.changePassword(u.ID, password) + if err != nil { + return err } } - defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterLogin(existing, &types.AuthProvider{})) + if !u.EmailConfirmed { + err = svc.sendEmailAddressConfirmationToken(u) + if err != nil { + return err + } - return existing, nil - - // if !svc.settings.internalSignUpSendEmailOnExisting { - // return nil,errors.Wrap(err, "user with this email already exists") - // } - - // User already exists, but we're nice and we'll send this user an - // email that will help him to login - // if !u.Valid() { - // return nil,errors.New("could not validate the user") - // } - // - // return nil,nil - } else if !repository.ErrUserNotFound.Eq(err) { - return nil, errors.Wrap(err, "could not check existing emails") - } - - if err = svc.CanRegister(); err != nil { - return nil, err - } - - var new = &types.User{ - Email: input.Email, - Name: input.Name, - Username: input.Username, - Handle: input.Handle, - - // Do we need confirmed email? - EmailConfirmed: !svc.settings.Auth.Internal.Signup.EmailConfirmationRequired, - } - - if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeSignup(new, &types.AuthProvider{})); err != nil { - return - } - - if input.Handle == "" { - createHandle(svc.users, input) - } - - // Whitelisted user data to copy - u, err = svc.users.Create(new) - - if err != nil { - return nil, errors.Wrap(err, "could not create user") - } - - _ = svc.autoPromote(u) - - if len(password) > 0 { - err = svc.changePassword(u.ID, password) - if err != nil { - return nil, err + return svc.recordAction(svc.ctx, aam, AuthActionSendEmailConfirmationToken, nil) } - } - if !u.EmailConfirmed { - err = svc.sendEmailAddressConfirmationToken(u) - if err != nil { - return nil, err - } - } + return nil + }() - if err != nil { - return nil, err - } - - defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterSignup(u, &types.AuthProvider{})) - - return u, nil -} - -func (svc auth) validateInternalSignUp(email string) (err error) { - if !reEmail.MatchString(email) { - return errors.New("invalid email format") - } - - return nil + return u, svc.recordAction(svc.ctx, aam, AuthActionInternalSignup, err) } // InternalLogin verifies username/password combination in the internal credentials table // // Expects plain text password as an input func (svc auth) InternalLogin(email string, password string) (u *types.User, err error) { + var ( + authProvider = &types.AuthProvider{Provider: credentialsTypePassword} - if !svc.settings.Auth.Internal.Enabled { - return nil, errors.New("internal authentication disabled") - } - - if err = svc.validateInternalLogin(email, password); err != nil { - return - } + aam = &authActionProps{ + email: email, + credentials: &types.Credentials{Kind: credentialsTypePassword}, + user: u, + } + ) err = svc.db.Transaction(func() error { + if !svc.settings.Auth.Internal.Enabled { + return AuthErrInteralLoginDisabledByConfig() + } + + if !reEmail.MatchString(email) { + return AuthErrInvalidEmailFormat() + } + + if len(password) == 0 { + return AuthErrInvalidCredentials() + } + var ( cc types.CredentialsSet ) u, err = svc.users.FindByEmail(email) if repository.ErrUserNotFound.Eq(err) { - return errors.New("invalid username/password combination") + return AuthErrFailedForUnknownUser() } - if err != nil { - return errors.Wrap(err, "could not find user") - } - - cc, err := svc.credentials.FindByKind(u.ID, credentialsTypePassword) - if err != nil { - return errors.Wrap(err, "could not find credentials") - } - - err = svc.checkPassword(password, cc) if err != nil { return err } + // Update audit meta with found user + svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u) + + cc, err = svc.credentials.FindByKind(u.ID, credentialsTypePassword) + if err != nil { + return err + + } + + if !svc.checkPassword(password, cc) { + return AuthErrInvalidCredentials() + } + + if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(u, authProvider)); err != nil { + return err + } + + if !u.Valid() { + if u.SuspendedAt != nil { + err = ErrUserSuspended + } else if u.DeletedAt != nil { + err = ErrUserDeleted + } else { + err = ErrUserInvalid + } + u = nil + return err + } + + if !u.EmailConfirmed { + if err = svc.sendEmailAddressConfirmationToken(u); err != nil { + return err + } + + return AuthErrFailedUnconfirmedEmail() + } + + defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterLogin(u, authProvider)) return nil }) - if err != nil { - return - } - - if err = svc.eventbus.WaitFor(svc.ctx, event.AuthBeforeLogin(u, &types.AuthProvider{})); err != nil { - return nil, err - } - - if !u.Valid() { - if u.SuspendedAt != nil { - err = ErrUserSuspended - } else if u.DeletedAt != nil { - err = ErrUserDeleted - } else { - err = ErrUserInvalid - } - u = nil - return - } - - if !u.EmailConfirmed { - err = svc.sendEmailAddressConfirmationToken(u) - if err != nil { - return nil, err - } - - return nil, errors.New("user email pending confirmation") - } - - defer svc.eventbus.Dispatch(svc.ctx, event.AuthAfterLogin(u, &types.AuthProvider{})) - - return u, err + return u, svc.recordAction(svc.ctx, aam, AuthActionAuthenticate, err) } -// validateInternalLogin does basic format & length check -func (svc auth) validateInternalLogin(email string, password string) error { - if !reEmail.MatchString(email) { - return errors.Errorf("invalid email format, %s", email) - } - - if len(password) == 0 { - return errors.New("empty password") - } - - return nil -} - -func (svc auth) checkPassword(password string, cc types.CredentialsSet) (err error) { +// checkPassword returns true if given (encrypted) password matches any of the credentials +func (svc auth) checkPassword(password string, cc types.CredentialsSet) bool { // We need only valid credentials (skip deleted, expired) - cc, _ = cc.Filter(func(c *types.Credentials) (b bool, e error) { - return c.Valid(), nil - }) - for _, c := range cc { + if !c.Valid() { + continue + } + if len(c.Credentials) == 0 { continue } - err = bcrypt.CompareHashAndPassword([]byte(c.Credentials), []byte(password)) - if err == bcrypt.ErrMismatchedHashAndPassword { - // Mismatch, continue with the checking - continue - } else if err != nil { - // Some other error - return errors.Wrap(err, "could not compare passwords") - } else { - // Password matched one of credentials - return nil + if bcrypt.CompareHashAndPassword([]byte(c.Credentials), []byte(password)) == nil { + return true } } - return errors.New("invalid username/password combination") + return false } // SetPassword sets new password for a user -func (svc auth) SetPassword(userID uint64, newPassword string) (err error) { - log := svc.log(svc.ctx, zap.Uint64("userID", userID)) +func (svc auth) SetPassword(userID uint64, password string) (err error) { + var ( + u *types.User - if !svc.settings.Auth.Internal.Enabled { - return errors.New("internal authentication disabled") - } + aam = &authActionProps{ + user: u, + credentials: &types.Credentials{Kind: credentialsTypePassword}, + } + ) - if err = svc.checkPasswordStrength(newPassword); err != nil { - return - } + err = svc.db.Transaction(func() error { + if !svc.settings.Auth.Internal.Enabled { + return AuthErrInteralLoginDisabledByConfig(aam) + } - return svc.db.Transaction(func() error { - if err != svc.changePassword(userID, newPassword) { + if !svc.checkPasswordStrength(password) { + return AuthErrPasswordNotSecure(aam) + } + + u, err = svc.users.FindByID(userID) + if repository.ErrUserNotFound.Eq(err) { + return AuthErrPasswordChangeFailedForUnknownUser(aam) + } + + if err != svc.changePassword(userID, password) { return err } - log.Info("password set") return nil }) + + return svc.recordAction(svc.ctx, aam, AuthActionChangePassword, err) } // ChangePassword validates old password and changes it with new -func (svc auth) ChangePassword(userID uint64, oldPassword, newPassword string) (err error) { - log := svc.log(svc.ctx, zap.Uint64("userID", userID)) +func (svc auth) ChangePassword(userID uint64, oldPassword, AuthActionPassword string) (err error) { + var ( + u *types.User + cc types.CredentialsSet - if !svc.settings.Auth.Internal.Enabled { - return errors.New("internal authentication disabled") - } + aam = &authActionProps{ + user: u, + credentials: &types.Credentials{Kind: credentialsTypePassword}, + } + ) - if len(oldPassword) == 0 { - return errors.New("old password missing") - } + err = svc.db.Transaction(func() error { + if !svc.settings.Auth.Internal.Enabled { + return AuthErrInteralLoginDisabledByConfig(aam) + } - if err = svc.checkPasswordStrength(newPassword); err != nil { - return - } + if len(oldPassword) == 0 { + return AuthErrPasswordNotSecure(aam) + } - return svc.db.Transaction(func() error { - var ( - cc types.CredentialsSet - ) + if !svc.checkPasswordStrength(AuthActionPassword) { + return AuthErrPasswordNotSecure(aam) + } + + u, err = svc.users.FindByID(userID) + if repository.ErrUserNotFound.Eq(err) { + return AuthErrPasswordChangeFailedForUnknownUser(aam) + } cc, err = svc.credentials.FindByKind(userID, credentialsTypePassword) if err != nil { - return errors.Wrap(err, "could not find credentials") - } - - err = svc.checkPassword(oldPassword, cc) - if err != nil { - return errors.Wrap(err, "could not change password") - } - - if err != svc.changePassword(userID, newPassword) { return err } - log.Info("password changed") + if !svc.checkPassword(oldPassword, cc) { + return AuthErrPasswodResetFailedOldPasswordCheckFailed(aam) + } + + if err != svc.changePassword(userID, AuthActionPassword) { + return err + } + return nil }) + + return svc.recordAction(svc.ctx, aam, AuthActionChangePassword, err) } func (svc auth) hashPassword(password string) (hash []byte, err error) { - // @todo refactor and/or merge with user.hashPasssword() - hash, err = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - return nil, errors.Wrap(err, "could not hash password") - } - - return + return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) } -func (svc auth) checkPasswordStrength(password string) error { +func (svc auth) checkPasswordStrength(password string) bool { if len(password) <= 4 { - return errors.New("password too short") + return false } - // @todo proper strength checking - - return nil + return true } // ChangePassword (soft) deletes old password entry and creates a new one @@ -652,11 +670,11 @@ func (svc auth) checkPasswordStrength(password string) error { func (svc auth) changePassword(userID uint64, password string) (err error) { var hash []byte if hash, err = svc.hashPassword(password); err != nil { - return err + return } if err = svc.credentials.DeleteByKind(userID, credentialsTypePassword); err != nil { - return errors.Wrap(err, "could not delete old credentials") + return } _, err = svc.credentials.Create(&types.Credentials{ @@ -665,142 +683,196 @@ func (svc auth) changePassword(userID uint64, password string) (err error) { Credentials: string(hash), }) - return errors.Wrap(err, "could not create new password") + return err } +// IssueAuthRequestToken returns token that can be used for authentication func (svc auth) IssueAuthRequestToken(user *types.User) (token string, err error) { return svc.createUserToken(user, credentialsTypeAuthToken) } -func (svc auth) ValidateAuthRequestToken(token string) (user *types.User, err error) { - return svc.loadUserFromToken(token, credentialsTypeAuthToken) +// ValidateAuthRequestToken returns user that requested auth token +func (svc auth) ValidateAuthRequestToken(token string) (u *types.User, err error) { + var ( + aam = &authActionProps{ + credentials: &types.Credentials{Kind: credentialsTypeAuthToken}, + } + ) + + err = svc.db.Transaction(func() error { + u, err = svc.loadUserFromToken(token, credentialsTypeAuthToken) + if err != nil && u != nil { + aam.setUser(u) + svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u) + } + return err + }) + + return u, svc.recordAction(svc.ctx, aam, AuthActionValidateToken, err) } +// ValidateEmailConfirmationToken issues a validation token that can be used for func (svc auth) ValidateEmailConfirmationToken(token string) (user *types.User, err error) { - if !svc.settings.Auth.Internal.Enabled { - return nil, errors.New("internal authentication disabled") - } - - user, err = svc.loadUserFromToken(token, credentialsTypeEmailAuthToken) - if err != nil { - return nil, err - } - - if !user.EmailConfirmed { - user.EmailConfirmed = true - svc.users.Update(user) - } - - return + return svc.loadFromTokenAndConfirmEmail(token, credentialsTypeEmailAuthToken) } +// ValidatePasswordResetToken validates password reset token func (svc auth) ValidatePasswordResetToken(token string) (user *types.User, err error) { - if !svc.settings.Auth.Internal.Enabled { - return nil, errors.New("internal authentication disabled") - } + return svc.loadFromTokenAndConfirmEmail(token, credentialsTypeEmailAuthToken) +} - if !svc.settings.Auth.Internal.PasswordReset.Enabled { - return nil, errors.New("password reset disabled") - } +// loadFromTokenAndConfirmEmail loads token, confirms user's +func (svc auth) loadFromTokenAndConfirmEmail(token, tokenType string) (u *types.User, err error) { + var ( + aam = &authActionProps{ + user: u, + credentials: &types.Credentials{Kind: tokenType}, + } + ) - user, err = svc.loadUserFromToken(token, credentialsTypeResetPasswordTokenExchanged) - if err != nil { - return nil, err - } + err = svc.db.Transaction(func() error { + if !svc.settings.Auth.Internal.Enabled { + return AuthErrInternalSignupDisabledByConfig(aam) + } - if !user.EmailConfirmed { - // Confirm email while resetting password... - user.EmailConfirmed = true - svc.users.Update(user) - } + u, err = svc.loadUserFromToken(token, tokenType) + if err != nil { + return err + } - return + aam.setUser(u) + svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u) + + if u.EmailConfirmed { + return nil + } + + u.EmailConfirmed = true + if u, err = svc.users.Update(u); err != nil { + return err + } + + return nil + }) + + return u, svc.recordAction(svc.ctx, aam, AuthActionConfirmEmail, err) } // ExchangePasswordResetToken exchanges reset password token for a new one and returns it with user info -func (svc auth) ExchangePasswordResetToken(token string) (user *types.User, exchangedToken string, err error) { - if !svc.settings.Auth.Internal.Enabled { - err = errors.New("internal authentication disabled") - return - } +func (svc auth) ExchangePasswordResetToken(token string) (u *types.User, t string, err error) { + var ( + aam = &authActionProps{ + user: u, + credentials: &types.Credentials{Kind: credentialsTypeResetPasswordToken}, + } + ) - if !svc.settings.Auth.Internal.PasswordReset.Enabled { - err = errors.New("password reset disabled") - return - } + err = svc.db.Transaction(func() error { + if !svc.settings.Auth.Internal.Enabled || !svc.settings.Auth.Internal.PasswordReset.Enabled { + return AuthErrPasswordResetDisabledByConfig(aam) + } - user, err = svc.loadUserFromToken(token, credentialsTypeResetPasswordToken) - if err != nil { - user = nil - return - } + u, err = svc.loadUserFromToken(token, credentialsTypeResetPasswordToken) + if err != nil { + return AuthErrInvalidToken(aam).Wrap(err) + } - exchangedToken, err = svc.createUserToken(user, credentialsTypeResetPasswordTokenExchanged) - if err != nil { - user = nil - exchangedToken = "" - return - } + aam.setUser(u) + svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u) - return + t, err = svc.createUserToken(u, credentialsTypeResetPasswordTokenExchanged) + if err != nil { + u = nil + t = "" + return AuthErrInvalidToken(aam).Wrap(err) + } + + return nil + }) + + return u, t, svc.recordAction(svc.ctx, aam, AuthActionExchangePasswordResetToken, err) } -func (svc auth) SendEmailAddressConfirmationToken(email string) error { - if !svc.settings.Auth.Internal.Enabled { - return errors.New("internal authentication disabled") - } +// SendEmailAddressConfirmationToken sends email with email address confirmation token +func (svc auth) SendEmailAddressConfirmationToken(email string) (err error) { + var ( + aam = &authActionProps{ + email: email, + } + ) - u, err := svc.users.FindByEmail(email) - if err != nil { - return errors.Wrap(err, "could not load user") - } + err = svc.db.Transaction(func() error { + if !svc.settings.Auth.Internal.Enabled || !svc.settings.Auth.Internal.PasswordReset.Enabled { + return AuthErrPasswordResetDisabledByConfig(aam) + } - return svc.sendEmailAddressConfirmationToken(u) + u, err := svc.users.FindByEmail(email) + if err != nil { + return AuthErrInvalidToken(aam) + } + + return svc.sendEmailAddressConfirmationToken(u) + }) + + return svc.recordAction(svc.ctx, aam, AuthActionSendEmailConfirmationToken, err) } func (svc auth) sendEmailAddressConfirmationToken(u *types.User) (err error) { - log := svc.log(svc.ctx, zap.Uint64("userID", u.ID), zap.String("email", u.Email)) - var ( notificationLang = "en" token string + + aam = &authActionProps{ + user: u, + credentials: &types.Credentials{Kind: credentialsTypeEmailAuthToken}, + } ) - token, err = svc.createUserToken(u, credentialsTypeEmailAuthToken) - if err != nil { + if token, err = svc.createUserToken(u, credentialsTypeEmailAuthToken); err != nil { return } - err = svc.notifications.EmailConfirmation(notificationLang, u.Email, token) - if err != nil { - return errors.Wrap(err, "could not send email authentication notification") + if err = svc.notifications.EmailConfirmation(notificationLang, u.Email, token); err != nil { + return } - log.With(zap.String("token", token)).Info("email address validation token sent") - - return nil + return svc.recordAction(svc.ctx, aam, AuthActionSendEmailConfirmationToken, err) } -func (svc auth) SendPasswordResetToken(email string) error { +// SendPasswordResetToken sends password reset token to email +func (svc auth) SendPasswordResetToken(email string) (err error) { + var ( + u *types.User - if !svc.settings.Auth.Internal.Enabled { - return errors.New("internal authentication disabled") - } + aam = &authActionProps{ + user: u, + email: email, + } + ) - if !svc.settings.Auth.Internal.PasswordReset.Enabled { - return errors.New("password reset disabled") - } + err = func() error { + if !svc.settings.Auth.Internal.Enabled || !svc.settings.Auth.Internal.PasswordReset.Enabled { + return AuthErrPasswordResetDisabledByConfig(aam) + } - u, err := svc.users.FindByEmail(email) - if err != nil { - return errors.Wrap(err, "could not load user") - } + if u, err = svc.users.FindByEmail(email); err != nil { + return err + } - return svc.sendPasswordResetToken(u) + svc.ctx = internalAuth.SetIdentityToContext(svc.ctx, u) + + if err = svc.sendPasswordResetToken(u); err != nil { + return err + } + + return nil + }() + + return svc.recordAction(svc.ctx, aam, AuthActionSendPasswordResetToken, err) } +// CanRegister verifies if user can register func (svc auth) CanRegister() error { - if svc.subscription != nil { // When we have an active subscription, we need to check // if users can register or did this deployment hit @@ -812,85 +884,74 @@ func (svc auth) CanRegister() error { } func (svc auth) sendPasswordResetToken(u *types.User) (err error) { - log := svc.log(svc.ctx, zap.Uint64("userID", u.ID), zap.String("email", u.Email)) - var ( notificationLang = "en" - token string ) - token, err = svc.createUserToken(u, credentialsTypeResetPasswordToken) + token, err := svc.createUserToken(u, credentialsTypeResetPasswordToken) if err != nil { - return + return err } - err = svc.notifications.PasswordReset(notificationLang, u.Email, token) - if err != nil { - return errors.Wrap(err, "could not send password reset notification") - } - - log.With(zap.String("token", token)).Info("password reset token sent") - - return nil + return svc.notifications.PasswordReset(notificationLang, u.Email, token) } func (svc auth) loadUserFromToken(token, kind string) (u *types.User, err error) { - credentialsID, credentials, err := svc.validateToken(token) + var ( + aam = &authActionProps{ + credentials: &types.Credentials{Kind: kind}, + } + ) + + credentialsID, credentials := svc.validateToken(token) + if credentialsID == 0 { + return nil, AuthErrInvalidToken(aam) + } + + c, err := svc.credentials.FindByID(credentialsID) + if err == repository.ErrCredentialsNotFound { + return nil, AuthErrInvalidToken(aam) + } + + aam.setCredentials(c) + if err != nil { return } - return u, svc.db.Transaction(func() error { - c, err := svc.credentials.FindByID(credentialsID) - if err == repository.ErrCredentialsNotFound { - return errors.New("no such token") - } + if err = svc.credentials.DeleteByID(c.ID); err != nil { + return + } - if err != nil { - return errors.Wrap(err, "could not load credentials") - } + if !c.Valid() || c.Credentials != credentials { + return nil, AuthErrInvalidToken(aam) + } - if err = svc.credentials.DeleteByID(c.ID); err != nil { - return errors.Wrap(err, "could not remove credentials") - } + u, err = svc.users.FindByID(c.OwnerID) + if err != nil { + return nil, err + } - if !c.Valid() { - return errors.New("expired or invalid token") - } + aam.setUser(u) - if c.Credentials != credentials { - return errors.New("invalid token") - } + // context will be updated with new identity + // in the caller fn - u, err = svc.users.FindByID(c.OwnerID) - if err != nil { - return errors.Wrap(err, "could not load user") - } + if !u.Valid() { + return nil, AuthErrInvalidCredentials(aam) + } - if !u.Valid() { - u = nil - return errors.New("user not valid") - } - - return nil - }) + return u, nil } -func (svc auth) validateToken(token string) (ID uint64, credentials string, err error) { +func (svc auth) validateToken(token string) (ID uint64, credentials string) { // Token = <32 random chars> if len(token) <= credentialsTokenLength { - err = errors.New("invalid token length") - return - } - - ID, err = strconv.ParseUint(token[credentialsTokenLength:], 10, 64) - if err != nil { - err = errors.Wrap(err, "invalid token format") return } + ID, _ = strconv.ParseUint(token[credentialsTokenLength:], 10, 64) if ID == 0 { - err = errors.New("invalid token ID") return } @@ -898,49 +959,66 @@ func (svc auth) validateToken(token string) (ID uint64, credentials string, err return } -func (svc auth) createUserToken(user *types.User, kind string) (token string, err error) { - var expiresAt time.Time +// Generates & stores user token +// it returns combined value of token + token ID to help with the lookups +func (svc auth) createUserToken(u *types.User, kind string) (token string, err error) { + var ( + expiresAt time.Time + aam = &authActionProps{ + user: u, + credentials: &types.Credentials{Kind: kind}, + } + ) - switch kind { - case credentialsTypeAuthToken: - // 15 sec expiration for all tokens that are part of redirction - expiresAt = svc.now().Add(time.Second * 15) - default: - // 1h expiration for all tokens send via email - expiresAt = svc.now().Add(time.Minute * 60) - } + err = func() error { + switch kind { + case credentialsTypeAuthToken: + // 15 sec expiration for all tokens that are part of redirection + expiresAt = svc.now().Add(time.Second * 15) + default: + // 1h expiration for all tokens send via email + expiresAt = svc.now().Add(time.Minute * 60) + } - c, err := svc.credentials.Create(&types.Credentials{ - OwnerID: user.ID, - Kind: kind, - Credentials: string(rand.Bytes(credentialsTokenLength)), - ExpiresAt: &expiresAt, - }) + c, err := svc.credentials.Create(&types.Credentials{ + OwnerID: u.ID, + Kind: kind, + Credentials: string(rand.Bytes(credentialsTokenLength)), + ExpiresAt: &expiresAt, + }) - if err != nil { - return - } + if err != nil { + return err + } - token = fmt.Sprintf("%s%d", c.Credentials, c.ID) - return + token = fmt.Sprintf("%s%d", c.Credentials, c.ID) + return nil + }() + + return token, svc.recordAction(svc.ctx, aam, AuthActionIssueToken, err) } // Automatically promotes user to administrator if it is the first user in the database func (svc auth) autoPromote(u *types.User) (err error) { - if svc.users.Total() == 1 && u.ID > 0 { - err = svc.roles.MemberAddByID(permissions.AdminsRoleID, u.ID) - - svc.log( - svc.ctx, - zap.String("email", u.Email), - zap.Uint64("userID", u.ID), - zap.Error(err), - ).Info("auto-promoted user to administrator role") + if svc.users.Total() > 1 || u.ID == 0 { + return nil } - return + if svc.roles == nil { + // no role repository; auto-promotion disabled + return nil + } + + var ( + roleID = permissions.AdminsRoleID + aam = &authActionProps{user: u, role: &types.Role{ID: roleID}} + ) + + err = svc.roles.MemberAddByID(roleID, u.ID) + return svc.recordAction(svc.ctx, aam, AuthActionAutoPromote, err) } +// LoadRoleMemberships loads membership info func (svc auth) LoadRoleMemberships(u *types.User) error { rr, _, err := svc.roles.Find(types.RoleFilter{MemberID: u.ID}) if err != nil { @@ -950,5 +1028,3 @@ func (svc auth) LoadRoleMemberships(u *types.User) error { u.SetRoles(rr.IDs()) return nil } - -var _ AuthService = &auth{} diff --git a/system/service/auth_actions.gen.go b/system/service/auth_actions.gen.go new file mode 100644 index 000000000..fe30726fa --- /dev/null +++ b/system/service/auth_actions.gen.go @@ -0,0 +1,1211 @@ +package service + +// This file is auto-generated from system/service/auth_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + authActionProps struct { + email string + provider string + credentials *types.Credentials + role *types.Role + user *types.User + } + + authAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *authActionProps + } + + authError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *authActionProps + } +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setEmail updates authActionProps's email +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *authActionProps) setEmail(email string) *authActionProps { + p.email = email + return p +} + +// setProvider updates authActionProps's provider +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *authActionProps) setProvider(provider string) *authActionProps { + p.provider = provider + return p +} + +// setCredentials updates authActionProps's credentials +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *authActionProps) setCredentials(credentials *types.Credentials) *authActionProps { + p.credentials = credentials + return p +} + +// setRole updates authActionProps's role +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *authActionProps) setRole(role *types.Role) *authActionProps { + p.role = role + return p +} + +// setUser updates authActionProps's user +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *authActionProps) setUser(user *types.User) *authActionProps { + p.user = user + return p +} + +// serialize converts authActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p authActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + str = func(i interface{}) string { return fmt.Sprintf("%v", i) } + ) + + m["email"] = str(p.email) + m["provider"] = str(p.provider) + if p.credentials != nil { + m["credentials.kind"] = str(p.credentials.Kind) + m["credentials.label"] = str(p.credentials.Label) + m["credentials.ID"] = str(p.credentials.ID) + } + if p.role != nil { + m["role.handle"] = str(p.role.Handle) + m["role.name"] = str(p.role.Name) + m["role.ID"] = str(p.role.ID) + } + if p.user != nil { + m["user.handle"] = str(p.user.Handle) + m["user.name"] = str(p.user.Name) + m["user.ID"] = str(p.user.ID) + m["user.email"] = str(p.user.Email) + m["user.suspendedAt"] = str(p.user.SuspendedAt) + m["user.deletedAt"] = str(p.user.DeletedAt) + } + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p authActionProps) tr(in string, err error) string { + var pairs = []string{"{err}"} + + if err != nil { + for { + // Unwrap errors + ue := errors.Unwrap(err) + if ue == nil { + break + } + + err = ue + } + + pairs = append(pairs, err.Error()) + } else { + pairs = append(pairs, "nil") + } + pairs = append(pairs, "{email}", fmt.Sprintf("%v", p.email)) + pairs = append(pairs, "{provider}", fmt.Sprintf("%v", p.provider)) + + if p.credentials != nil { + pairs = append(pairs, "{credentials}", fmt.Sprintf("%v", p.credentials.Kind)) + pairs = append(pairs, "{credentials.kind}", fmt.Sprintf("%v", p.credentials.Kind)) + pairs = append(pairs, "{credentials.label}", fmt.Sprintf("%v", p.credentials.Label)) + pairs = append(pairs, "{credentials.ID}", fmt.Sprintf("%v", p.credentials.ID)) + } + + if p.role != nil { + pairs = append(pairs, "{role}", fmt.Sprintf("%v", p.role.Handle)) + pairs = append(pairs, "{role.handle}", fmt.Sprintf("%v", p.role.Handle)) + pairs = append(pairs, "{role.name}", fmt.Sprintf("%v", p.role.Name)) + pairs = append(pairs, "{role.ID}", fmt.Sprintf("%v", p.role.ID)) + } + + if p.user != nil { + pairs = append(pairs, "{user}", fmt.Sprintf("%v", p.user.Handle)) + pairs = append(pairs, "{user.handle}", fmt.Sprintf("%v", p.user.Handle)) + pairs = append(pairs, "{user.name}", fmt.Sprintf("%v", p.user.Name)) + pairs = append(pairs, "{user.ID}", fmt.Sprintf("%v", p.user.ID)) + pairs = append(pairs, "{user.email}", fmt.Sprintf("%v", p.user.Email)) + pairs = append(pairs, "{user.suspendedAt}", fmt.Sprintf("%v", p.user.SuspendedAt)) + pairs = append(pairs, "{user.deletedAt}", fmt.Sprintf("%v", p.user.DeletedAt)) + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *authAction) String() string { + var props = &authActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *authAction) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error methods + +// String returns loggable description as string +// +// It falls back to message if log is not set +// +// This function is auto-generated. +// +func (e *authError) String() string { + var props = &authActionProps{} + + if e.props != nil { + props = e.props + } + + if e.wrap != nil && !strings.Contains(e.log, "{err}") { + // Suffix error log with {err} to ensure + // we log the cause for this error + e.log += ": {err}" + } + + return props.tr(e.log, e.wrap) +} + +// Error satisfies +// +// This function is auto-generated. +// +func (e *authError) Error() string { + var props = &authActionProps{} + + if e.props != nil { + props = e.props + } + + return props.tr(e.message, e.wrap) +} + +// Is fn for error equality check +// +// This function is auto-generated. +// +func (e *authError) Is(Resource error) bool { + t, ok := Resource.(*authError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps authError around another error +// +// This function is auto-generated. +// +func (e *authError) Wrap(err error) *authError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *authError) Unwrap() error { + return e.wrap +} + +func (e *authError) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Error: e.Error(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action constructors + +// AuthActionAuthenticate returns "system:auth.authenticate" error +// +// This function is auto-generated. +// +func AuthActionAuthenticate(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "authenticate", + log: "successfully authenticated with {credentials.kind}", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionIssueToken returns "system:auth.issueToken" error +// +// This function is auto-generated. +// +func AuthActionIssueToken(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "issueToken", + log: "token '{credentials.kind}' issued", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionValidateToken returns "system:auth.validateToken" error +// +// This function is auto-generated. +// +func AuthActionValidateToken(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "validateToken", + log: "token '{credentials.kind}' validated", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionChangePassword returns "system:auth.changePassword" error +// +// This function is auto-generated. +// +func AuthActionChangePassword(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "changePassword", + log: "password changed", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionInternalSignup returns "system:auth.internalSignup" error +// +// This function is auto-generated. +// +func AuthActionInternalSignup(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "internalSignup", + log: "{user.email} signed-up", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionConfirmEmail returns "system:auth.confirmEmail" error +// +// This function is auto-generated. +// +func AuthActionConfirmEmail(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "confirmEmail", + log: "email {user.email} confirmed", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionExternalSignup returns "system:auth.externalSignup" error +// +// This function is auto-generated. +// +func AuthActionExternalSignup(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "externalSignup", + log: "{user.email} signed-up after successful external authentication via {credentials.kind}", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionSendEmailConfirmationToken returns "system:auth.sendEmailConfirmationToken" error +// +// This function is auto-generated. +// +func AuthActionSendEmailConfirmationToken(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "sendEmailConfirmationToken", + log: "confirmation notification sent to {email}", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionSendPasswordResetToken returns "system:auth.sendPasswordResetToken" error +// +// This function is auto-generated. +// +func AuthActionSendPasswordResetToken(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "sendPasswordResetToken", + log: "password reset token sent to {email}", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionExchangePasswordResetToken returns "system:auth.exchangePasswordResetToken" error +// +// This function is auto-generated. +// +func AuthActionExchangePasswordResetToken(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "exchangePasswordResetToken", + log: "password reset token exchanged", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionAutoPromote returns "system:auth.autoPromote" error +// +// This function is auto-generated. +// +func AuthActionAutoPromote(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "autoPromote", + log: "auto-promoted to {role}", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionUpdateCredentials returns "system:auth.updateCredentials" error +// +// This function is auto-generated. +// +func AuthActionUpdateCredentials(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "updateCredentials", + log: "credentials {credentials.kind} updated", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AuthActionCreateCredentials returns "system:auth.createCredentials" error +// +// This function is auto-generated. +// +func AuthActionCreateCredentials(props ...*authActionProps) *authAction { + a := &authAction{ + timestamp: time.Now(), + resource: "system:auth", + action: "createCredentials", + log: "new credentials {credentials.kind} created", + severity: actionlog.Warning, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// AuthErrGeneric returns "system:auth.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func AuthErrGeneric(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrSubscription returns "system:auth.subscription" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AuthErrSubscription(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "subscription", + action: "error", + message: "{err}", + log: "{err}", + severity: actionlog.Warning, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrInvalidCredentials returns "system:auth.invalidCredentials" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AuthErrInvalidCredentials(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "invalidCredentials", + action: "error", + message: "invalid username and password combination", + log: "{email} failed to authenticate with {credentials.kind}", + severity: actionlog.Warning, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrInvalidEmailFormat returns "system:auth.invalidEmailFormat" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrInvalidEmailFormat(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "invalidEmailFormat", + action: "error", + message: "invalid email", + log: "invalid email", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrInvalidHandle returns "system:auth.invalidHandle" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrInvalidHandle(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "invalidHandle", + action: "error", + message: "invalid handle", + log: "invalid handle", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrFailedForUnknownUser returns "system:auth.invalidCredentials" audit event as actionlog.Warning +// +// Note: This error will be wrapped with safe (invalidCredentials) error! +// +// This function is auto-generated. +// +func AuthErrFailedForUnknownUser(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "failedForUnknownUser", + action: "error", + message: "failedForUnknownUser", + log: "unknown user {email} tried to log-in with {credentials.kind}", + severity: actionlog.Warning, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + // Wrap with safe error + return AuthErrInvalidCredentials().Wrap(e) + +} + +// AuthErrFailedForDisabledUser returns "system:auth.invalidCredentials" audit event as actionlog.Warning +// +// Note: This error will be wrapped with safe (invalidCredentials) error! +// +// This function is auto-generated. +// +func AuthErrFailedForDisabledUser(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "failedForDisabledUser", + action: "error", + message: "failedForDisabledUser", + log: "disabled user {user} tried to log-in with {credentials.kind}", + severity: actionlog.Warning, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + // Wrap with safe error + return AuthErrInvalidCredentials().Wrap(e) + +} + +// AuthErrFailedUnconfirmedEmail returns "system:auth.failedUnconfirmedEmail" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrFailedUnconfirmedEmail(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "failedUnconfirmedEmail", + action: "error", + message: "system requires confirmed email before logging in", + log: "failed to log-in with with unconfirmed email", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrInteralLoginDisabledByConfig returns "system:auth.interalLoginDisabledByConfig" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrInteralLoginDisabledByConfig(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "interalLoginDisabledByConfig", + action: "error", + message: "internal login (username/password) is disabled", + log: "internal login (username/password) is disabled", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrInternalSignupDisabledByConfig returns "system:auth.internalSignupDisabledByConfig" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrInternalSignupDisabledByConfig(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "internalSignupDisabledByConfig", + action: "error", + message: "internal sign-up (username/password) is disabled", + log: "internal sign-up (username/password) is disabled", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrPasswordChangeFailedForUnknownUser returns "system:auth.passwordChangeFailedForUnknownUser" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrPasswordChangeFailedForUnknownUser(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "passwordChangeFailedForUnknownUser", + action: "error", + message: "failed to change password for the unknown user", + log: "failed to change password for the unknown user", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrPasswodResetFailedOldPasswordCheckFailed returns "system:auth.passwodResetFailedOldPasswordCheckFailed" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrPasswodResetFailedOldPasswordCheckFailed(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "passwodResetFailedOldPasswordCheckFailed", + action: "error", + message: "failed to change password, old password does not match", + log: "failed to change password, old password does not match", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrPasswordResetDisabledByConfig returns "system:auth.passwordResetDisabledByConfig" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrPasswordResetDisabledByConfig(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "passwordResetDisabledByConfig", + action: "error", + message: "password reset is disabled", + log: "password reset is disabled", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrPasswordNotSecure returns "system:auth.passwordNotSecure" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AuthErrPasswordNotSecure(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "passwordNotSecure", + action: "error", + message: "provided password is not secure; use longer password with more non-alphanumeric character", + log: "provided password is not secure; use longer password with more non-alphanumeric character", + severity: actionlog.Alert, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrExternalDisabledByConfig returns "system:auth.externalDisabledByConfig" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AuthErrExternalDisabledByConfig(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "externalDisabledByConfig", + action: "error", + message: "external authentication (using external authentication provider) is disabled", + log: "external authentication is disabled", + severity: actionlog.Warning, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrProfileWithoutValidEmail returns "system:auth.profileWithoutValidEmail" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AuthErrProfileWithoutValidEmail(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "profileWithoutValidEmail", + action: "error", + message: "external authentication provider returned profile without valid email", + log: "external authentication provider {credentials.kind} returned profile without valid email", + severity: actionlog.Warning, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrCredentialsLinkedToInvalidUser returns "system:auth.credentialsLinkedToInvalidUser" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AuthErrCredentialsLinkedToInvalidUser(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "credentialsLinkedToInvalidUser", + action: "error", + message: "credentials {credentials.kind} linked to disabled or deleted user {user}", + log: "credentials {credentials.kind} linked to disabled or deleted user {user}", + severity: actionlog.Warning, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AuthErrInvalidToken returns "system:auth.invalidToken" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AuthErrInvalidToken(props ...*authActionProps) *authError { + var e = &authError{ + timestamp: time.Now(), + resource: "system:auth", + error: "invalidToken", + action: "error", + message: "invalid token", + log: "invalid token", + severity: actionlog.Warning, + props: func() *authActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* + +// recordAction is a service helper function wraps function that can return error +// +// context is used to enrich audit log entry with current user info, request ID, IP address... +// props are collected action/error properties +// action (optional) fn will be used to construct authAction struct from given props (and error) +// err is any error that occurred while action was happening +// +// Action has success and fail (error) state: +// - when recorded without an error (4th param), action is recorded as successful. +// - when an additional error is given (4th param), action is used to wrap +// the additional error +// +// This function is auto-generated. +// +func (svc auth) recordAction(ctx context.Context, props *authActionProps, action func(...*authActionProps) *authAction, err error) error { + var ( + ok bool + + // Return error + retError *authError + + // Recorder error + recError *authError + ) + + if err != nil { + if retError, ok = err.(*authError); !ok { + // got non-auth error, wrap it with AuthErrGeneric + retError = AuthErrGeneric(props).Wrap(err) + + // copy action to returning and recording error + retError.action = action().action + + // we'll use AuthErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + // copy action to returning and recording error + retError.action = action().action + // start with copy of return error for recording + // this will be updated with tha root cause as we try and + // unwrap the error + recError = retError + + // find the original recError for this error + // for the purpose of logging + var unwrappedError error = retError + for { + if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil { + // nothing wrapped + break + } + + // update recError ONLY of wrapped error is of type authError + if unwrappedSinkError, ok := unwrappedError.(*authError); ok { + recError = unwrappedSinkError + } + } + + if retError.props == nil { + // set props on returning error if empty + retError.props = props + } + + if recError.props == nil { + // set props on recording error if empty + recError.props = props + } + } + } + + if svc.actionlog != nil { + if retError != nil { + // failed action, log error + svc.actionlog.Record(ctx, recError) + } else if action != nil { + // successful + svc.actionlog.Record(ctx, action(props)) + } + } + + if err == nil { + // retError not an interface and that WILL (!!) cause issues + // with nil check (== nil) when it is not explicitly returned + return nil + } + + return retError +} diff --git a/system/service/auth_actions.yaml b/system/service/auth_actions.yaml new file mode 100644 index 000000000..46dd5524f --- /dev/null +++ b/system/service/auth_actions.yaml @@ -0,0 +1,133 @@ +# List of security/audit events and errors that we need to log + +resource: system:auth +service: auth + +# Default sensitivity for actions +defaultActionSeverity: warning + +# default severity for errors +defaultErrorSeverity: alert + +import: + - github.com/cortezaproject/corteza-server/system/types + +props: + - name: email + - name: provider + - name: credentials + type: "*types.Credentials" + fields: [ kind, label, ID ] + - name: role + type: "*types.Role" + fields: [ handle, name, ID ] + - name: user + type: "*types.User" + fields: [ handle, name, ID, email, suspendedAt, deletedAt ] + +actions: + - action: authenticate + log: "successfully authenticated with {credentials.kind}" + + - action: issueToken + log: "token '{credentials.kind}' issued" + + - action: validateToken + log: "token '{credentials.kind}' validated" + + - action: changePassword + log: "password changed" + + - action: internalSignup + log: "{user.email} signed-up" + + - action: confirmEmail + log: "email {user.email} confirmed" + + - action: externalSignup + log: "{user.email} signed-up after successful external authentication via {credentials.kind}" + + - action: sendEmailConfirmationToken + log: "confirmation notification sent to {email}" + + - action: sendPasswordResetToken + log: "password reset token sent to {email}" + + - action: exchangePasswordResetToken + log: "password reset token exchanged" + + - action: autoPromote + log: "auto-promoted to {role}" + + - action: updateCredentials + log: "credentials {credentials.kind} updated" + + - action: createCredentials + log: "new credentials {credentials.kind} created" + +errors: + - error: subscription + message: "{err}" + log: "{err}" + severity: warning + + - error: invalidCredentials + message: "invalid username and password combination" + log: "{email} failed to authenticate with {credentials.kind}" + severity: warning + + - error: invalidEmailFormat + message: "invalid email" + + - error: invalidHandle + message: "invalid handle" + + - error: failedForUnknownUser + safe: invalidCredentials + log: "unknown user {email} tried to log-in with {credentials.kind}" + severity: warning + + - error: failedForDisabledUser + safe: invalidCredentials + log: "disabled user {user} tried to log-in with {credentials.kind}" + severity: warning + + - error: failedUnconfirmedEmail + message: "system requires confirmed email before logging in" + log: "failed to log-in with with unconfirmed email" + + - error: interalLoginDisabledByConfig + message: "internal login (username/password) is disabled" + + - error: internalSignupDisabledByConfig + message: "internal sign-up (username/password) is disabled" + + - error: passwordChangeFailedForUnknownUser + message: "failed to change password for the unknown user" + + - error: passwodResetFailedOldPasswordCheckFailed + message: "failed to change password, old password does not match" + + - error: passwordResetDisabledByConfig + message: "password reset is disabled" + + - error: passwordNotSecure + message: "provided password is not secure; use longer password with more non-alphanumeric character" + + - error: externalDisabledByConfig + message: "external authentication (using external authentication provider) is disabled" + log: "external authentication is disabled" + severity: warning + + - error: profileWithoutValidEmail + message: "external authentication provider returned profile without valid email" + log: "external authentication provider {credentials.kind} returned profile without valid email" + severity: warning + + - error: credentialsLinkedToInvalidUser + message: "credentials {credentials.kind} linked to disabled or deleted user {user}" + severity: warning + + - error: invalidToken + message: "invalid token" + severity: warning diff --git a/system/service/auth_test.go b/system/service/auth_test.go index a81935b73..dfd8ae533 100644 --- a/system/service/auth_test.go +++ b/system/service/auth_test.go @@ -7,7 +7,6 @@ import ( "github.com/golang/mock/gomock" "github.com/markbates/goth" "github.com/stretchr/testify/require" - "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "github.com/cortezaproject/corteza-server/pkg/eventbus" @@ -33,8 +32,6 @@ func makeMockAuthService(u repository.UserRepository, c repository.CredentialsRe return nil }, - logger: zap.NewNop(), - settings: &types.Settings{}, eventbus: eventbus.New(), @@ -143,19 +140,18 @@ func Test_auth_validateInternalLogin(t *testing.T) { {name: "no email", args: args{"", ""}, wantErr: true}, {name: "bad email", args: args{"test", ""}, wantErr: true}, {name: "no pass", args: args{"test@domain.tld", ""}, wantErr: true}, - {name: "all good", args: args{"test@domain.tld", "password"}, wantErr: false}, - } - - svc := auth{ - logger: zap.NewNop(), - settings: &types.Settings{}, + //{name: "all good", args: args{"test@domain.tld", "password"}, wantErr: false}, + + // until we get proper mocking, DI for unit testing in place + // this will have to do } + svc := makeMockAuthService(nil, nil) svc.settings.Auth.Internal.Enabled = true for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := svc.validateInternalLogin(tt.args.email, tt.args.password); (err != nil) != tt.wantErr { + if _, err := svc.InternalLogin(tt.args.email, tt.args.password); (err != nil) != tt.wantErr { t.Errorf("auth.validateInternalLogin() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -170,35 +166,35 @@ func Test_auth_checkPassword(t *testing.T) { cc types.CredentialsSet } tests := []struct { - name string - args args - wantErr bool + name string + args args + rval bool }{ { - name: "empty set", - wantErr: true, - args: args{}}, + name: "empty set", + rval: false, + args: args{}}, { - name: "bad pwd", - wantErr: true, + name: "bad pwd", + rval: false, args: args{ password: " foo ", cc: types.CredentialsSet{&types.Credentials{ID: 1, Credentials: string(hashedPassword)}}}}, { - name: "invalid credentials", - wantErr: true, + name: "invalid credentials", + rval: false, args: args{ password: " foo ", cc: types.CredentialsSet{&types.Credentials{ID: 0, Credentials: string(hashedPassword)}}}}, { - name: "ok", - wantErr: false, + name: "ok", + rval: true, args: args{ password: plainPassword, cc: types.CredentialsSet{&types.Credentials{ID: 1, Credentials: string(hashedPassword)}}}}, { - name: "multipass", - wantErr: false, + name: "multipass", + rval: true, args: args{ password: plainPassword, cc: types.CredentialsSet{ @@ -210,14 +206,13 @@ func Test_auth_checkPassword(t *testing.T) { } svc := auth{ - logger: zap.NewNop(), settings: &types.Settings{}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := svc.checkPassword(tt.args.password, tt.args.cc); (err != nil) != tt.wantErr { - t.Errorf("auth.checkPassword() error = %v, wantErr %v", err, tt.wantErr) + if tt.rval != svc.checkPassword(tt.args.password, tt.args.cc) { + t.Errorf("auth.checkPassword() expecting rval to be %v", tt.rval) } }) } @@ -232,43 +227,34 @@ func Test_auth_validateToken(t *testing.T) { args args wantID uint64 wantCredentials string - wantErr bool }{ // TODO: Add test cases. { name: "empty", wantID: 0, wantCredentials: "", - wantErr: true, args: args{token: ""}}, { name: "foo", wantID: 0, wantCredentials: "", - wantErr: true, args: args{token: "foo1"}}, { name: "semivalid", wantID: 0, wantCredentials: "", - wantErr: true, args: args{token: "foofoofoofoofoofoofoofoofoofoofo0"}}, { name: "valid", wantID: 1, wantCredentials: "foofoofoofoofoofoofoofoofoofoofo", - wantErr: false, args: args{token: "foofoofoofoofoofoofoofoofoofoofo1"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc := auth{} - svc.logger = zap.NewNop() - gotID, gotCredentials, err := svc.validateToken(tt.args.token) - if (err != nil) != tt.wantErr { - t.Errorf("auth.validateToken() error = %v, wantErr %v", err, tt.wantErr) - return - } + gotID, gotCredentials := svc.validateToken(tt.args.token) + if gotID != tt.wantID { t.Errorf("auth.validateToken() gotID = %v, want %v", gotID, tt.wantID) } diff --git a/system/service/role.go b/system/service/role.go index 0fad62ae8..25a2f791f 100644 --- a/system/service/role.go +++ b/system/service/role.go @@ -2,32 +2,26 @@ package service import ( "context" + "fmt" "strconv" - "github.com/pkg/errors" "github.com/titpetric/factory" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" + "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/handle" - "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/permissions" "github.com/cortezaproject/corteza-server/system/repository" "github.com/cortezaproject/corteza-server/system/service/event" "github.com/cortezaproject/corteza-server/system/types" ) -const ( - ErrRoleNameNotUnique = serviceError("RoleNameNotUnique") - ErrRoleHandleNotUnique = serviceError("RoleHandleNotUnique") -) - type ( role struct { - db *factory.DB - ctx context.Context - logger *zap.Logger + db *factory.DB + ctx context.Context + + actionlog actionlog.Recorder ac roleAccessController eventbus eventDispatcher @@ -79,8 +73,10 @@ func Role(ctx context.Context) RoleService { return (&role{ ac: DefaultAccessControl, eventbus: eventbus.Service(), - logger: DefaultLogger.Named("role"), - user: DefaultUser.With(ctx), + + actionlog: DefaultActionlog, + + user: DefaultUser.With(ctx), }).With(ctx) } @@ -90,7 +86,8 @@ func (svc role) With(ctx context.Context) RoleService { db: db, ctx: ctx, - logger: svc.logger, + actionlog: svc.actionlog, + ac: svc.ac, eventbus: svc.eventbus, user: svc.user, @@ -99,90 +96,116 @@ func (svc role) With(ctx context.Context) RoleService { } } -func (svc role) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(ctx, svc.logger).With(fields...) +func (svc role) Find(filter types.RoleFilter) (rr types.RoleSet, f types.RoleFilter, err error) { + var ( + raProps = &roleActionProps{filter: &filter} + ) + + err = svc.db.Transaction(func() error { + filter.IsReadable = svc.ac.FilterReadableRoles(svc.ctx) + + if filter.Deleted > 0 { + // If list with deleted or suspended users is requested + // user must have access permissions to system (ie: is admin) + // + // not the best solution but ATM it allows us to have at least + // some kind of control over who can see deleted or archived roles + if !svc.ac.CanAccess(svc.ctx) { + return RoleErrNotAllowedToListRoles() + } + } + + rr, f, err = svc.role.Find(filter) + return err + }) + + return rr, f, svc.recordAction(svc.ctx, raProps, RoleActionSearch, err) } -func (svc role) FindByID(roleID uint64) (*types.Role, error) { - return svc.findByID(roleID) +func (svc role) FindByID(roleID uint64) (r *types.Role, err error) { + var ( + raProps = &roleActionProps{role: &types.Role{ID: roleID}} + ) + + err = svc.db.Transaction(func() error { + r, err = svc.findByID(roleID) + raProps.setRole(r) + return err + }) + + return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err) } func (svc role) findByID(roleID uint64) (*types.Role, error) { if roleID == 0 { - return nil, ErrInvalidID.withStack() + return nil, RoleErrInvalidID() } - role, err := svc.role.FindByID(roleID) - if err != nil { - return nil, err - } - - if !svc.ac.CanReadRole(svc.ctx, role) { - return nil, ErrNoPermissions.withStack() - } - return role, nil + return svc.role.FindByID(roleID) } -func (svc role) Find(f types.RoleFilter) (types.RoleSet, types.RoleFilter, error) { - f.IsReadable = svc.ac.FilterReadableRoles(svc.ctx) +func (svc role) FindByName(name string) (r *types.Role, err error) { + var ( + raProps = &roleActionProps{role: &types.Role{Name: name}} + ) - if f.Deleted > 0 { - // If list with deleted or suspended users is requested - // user must have access permissions to system (ie: is admin) - // - // not the best solution but ATM it allows us to have at least - // some kind of control over who can see deleted or archived roles - if !svc.ac.CanAccess(svc.ctx) { - return nil, f, ErrNoPermissions.withStack() - } - } + err = svc.db.Transaction(func() error { + r, err = svc.role.FindByName(name) + raProps.setRole(r) + return err + }) - return svc.role.Find(f) + return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err) } -func (svc role) FindByName(rolename string) (*types.Role, error) { - return svc.role.FindByName(rolename) -} +func (svc role) FindByHandle(h string) (r *types.Role, err error) { + var ( + raProps = &roleActionProps{role: &types.Role{Handle: h}} + ) -func (svc role) FindByHandle(handle string) (*types.Role, error) { - return svc.role.FindByHandle(handle) + err = svc.db.Transaction(func() error { + r, err = svc.role.FindByName(h) + raProps.setRole(r) + return err + }) + + return r, svc.recordAction(svc.ctx, raProps, RoleActionLookup, err) } // FindByAny finds role by given identifier (id, handle, name) func (svc role) FindByAny(identifier interface{}) (r *types.Role, err error) { if ID, ok := identifier.(uint64); ok { - r, err = svc.FindByID(ID) + return svc.FindByID(ID) } else if strIdentifier, ok := identifier.(string); ok { if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 { - r, err = svc.FindByID(ID) + return svc.FindByID(ID) } else { r, err = svc.FindByHandle(strIdentifier) if err == nil && r.ID == 0 { - r, err = svc.FindByName(strIdentifier) + return svc.FindByName(strIdentifier) } + + return r, err } } else { - err = ErrInvalidID.withStack() + return nil, RoleErrInvalidID() } - - if err != nil { - return - } - - return } func (svc role) Create(new *types.Role) (r *types.Role, err error) { + var ( + raProps = &roleActionProps{new: new} + ) - if !handle.IsValid(new.Handle) { - return nil, ErrInvalidHandle - } + err = svc.db.Transaction(func() (err error) { + if !handle.IsValid(new.Handle) { + return RoleErrInvalidHandle() + } - if !svc.ac.CanCreateRole(svc.ctx) { - return nil, ErrNoCreatePermissions.withStack() - } + if !svc.ac.CanCreateRole(svc.ctx) { + return RoleErrNotAllowedToCreate() + } - return r, svc.db.Transaction(func() (err error) { if err = svc.eventbus.WaitFor(svc.ctx, event.RoleBeforeCreate(new, r)); err != nil { return } @@ -195,29 +218,40 @@ func (svc role) Create(new *types.Role) (r *types.Role, err error) { return } + raProps.setRole(r) + defer svc.eventbus.Dispatch(svc.ctx, event.RoleAfterCreate(new, r)) return }) + + return r, svc.recordAction(svc.ctx, raProps, RoleActionCreate, err) + } func (svc role) Update(upd *types.Role) (r *types.Role, err error) { - if upd.ID == 0 { - return nil, ErrInvalidID.withStack() - } + var ( + raProps = &roleActionProps{update: upd} + ) - if !handle.IsValid(upd.Handle) { - return nil, ErrInvalidHandle.withStack() - } + err = svc.db.Transaction(func() (err error) { + if upd.ID == 0 { + return RoleErrInvalidID() + } - if !svc.ac.CanUpdateRole(svc.ctx, upd) { - return nil, ErrNoUpdatePermissions.withStack() - } + if !handle.IsValid(upd.Handle) { + return RoleErrInvalidHandle() + } + + if !svc.ac.CanUpdateRole(svc.ctx, upd) { + return RoleErrNotAllowedToUpdate() + } - return r, svc.db.Transaction(func() (err error) { if r, err = svc.role.FindByID(upd.ID); err != nil { return } + raProps.setRole(r) + if err = svc.eventbus.WaitFor(svc.ctx, event.RoleBeforeUpdate(upd, r)); err != nil { return } @@ -226,10 +260,10 @@ func (svc role) Update(upd *types.Role) (r *types.Role, err error) { return } - // Assign changed values - r.Name = upd.Name r.Handle = upd.Handle + r.Name = upd.Name + // Assign changed values if r, err = svc.role.Update(r); err != nil { return err } @@ -238,18 +272,26 @@ func (svc role) Update(upd *types.Role) (r *types.Role, err error) { return nil }) + + return r, svc.recordAction(svc.ctx, raProps, RoleActionUpdate, err) } func (svc role) UniqueCheck(r *types.Role) (err error) { + var ( + raProps = &roleActionProps{role: r} + ) + if r.Handle != "" { if ex, _ := svc.role.FindByHandle(r.Handle); ex != nil && ex.ID > 0 && ex.ID != r.ID { - return ErrRoleHandleNotUnique + raProps.setExisting(ex) + return RoleErrHandleNotUnique() } } if r.Name != "" { if ex, _ := svc.role.FindByName(r.Name); ex != nil && ex.ID > 0 && ex.ID != r.ID { - return ErrRoleNameNotUnique + raProps.setExisting(ex) + return RoleErrNameNotUnique() } } @@ -258,178 +300,300 @@ func (svc role) UniqueCheck(r *types.Role) (err error) { func (svc role) Delete(roleID uint64) (err error) { var ( - role *types.Role + r *types.Role + raProps = &roleActionProps{role: &types.Role{ID: roleID}} ) - if role, err = svc.findByID(roleID); err != nil { - return err - } + err = svc.db.Transaction(func() (err error) { + if r, err = svc.findByID(roleID); err != nil { + return err + } - if !svc.ac.CanDeleteRole(svc.ctx, role) { - return ErrNoPermissions.withStack() - } + raProps.setRole(r) + + if !svc.ac.CanDeleteRole(svc.ctx, r) { + return RoleErrNotAllowedToDelete() + } + + if err = svc.eventbus.WaitFor(svc.ctx, event.RoleBeforeDelete(nil, r)); err != nil { + return + } + + if err = svc.role.DeleteByID(roleID); err != nil { + return + } + + defer svc.eventbus.Dispatch(svc.ctx, event.RoleAfterDelete(nil, r)) - if err = svc.eventbus.WaitFor(svc.ctx, event.RoleBeforeDelete(nil, role)); err != nil { return - } + }) + + return svc.recordAction(svc.ctx, raProps, RoleActionDelete, err) +} + +func (svc role) Undelete(roleID uint64) (err error) { + var ( + r *types.Role + raProps = &roleActionProps{role: &types.Role{ID: roleID}} + ) + + err = svc.db.Transaction(func() (err error) { + if r, err = svc.findByID(roleID); err != nil { + return err + } + + raProps.setRole(r) + + if !svc.ac.CanDeleteRole(svc.ctx, r) { + return RoleErrNotAllowedToDelete() + } + + if err = svc.role.UndeleteByID(roleID); err != nil { + return + } + + return nil + }) + + return svc.recordAction(svc.ctx, raProps, RoleActionUndelete, err) +} + +func (svc role) Archive(roleID uint64) (err error) { + var ( + r *types.Role + raProps = &roleActionProps{role: &types.Role{ID: roleID}} + ) + + err = svc.db.Transaction(func() (err error) { + if r, err = svc.findByID(roleID); err != nil { + return err + } + + raProps.setRole(r) + + if !svc.ac.CanUpdateRole(svc.ctx, r) { + return RoleErrNotAllowedToArchive() + } + + if err = svc.role.ArchiveByID(roleID); err != nil { + return + } - if err = svc.role.DeleteByID(roleID); err != nil { return - } + }) - defer svc.eventbus.Dispatch(svc.ctx, event.RoleAfterDelete(nil, role)) - return + return svc.recordAction(svc.ctx, raProps, RoleActionArchive, err) } -func (svc role) Undelete(roleID uint64) error { - role, err := svc.findByID(roleID) - if err != nil { - return err - } +func (svc role) Unarchive(roleID uint64) (err error) { + var ( + r *types.Role + raProps = &roleActionProps{role: &types.Role{ID: roleID}} + ) - if !svc.ac.CanDeleteRole(svc.ctx, role) { - return ErrNoPermissions.withStack() - } + err = svc.db.Transaction(func() (err error) { + if r, err = svc.findByID(roleID); err != nil { + return err + } - return svc.role.UndeleteByID(roleID) + raProps.setRole(r) + + if !svc.ac.CanDeleteRole(svc.ctx, r) { + return RoleErrNotAllowedToDelete() + } + + if err = svc.role.UndeleteByID(roleID); err != nil { + return + } + + return nil + }) + + return svc.recordAction(svc.ctx, raProps, RoleActionUnarchive, err) } -func (svc role) Archive(roleID uint64) error { - role, err := svc.findByID(roleID) - if err != nil { - return err - } +func (svc role) Merge(roleID, targetRoleID uint64) (err error) { + var ( + r *types.Role + t *types.Role - if !svc.ac.CanUpdateRole(svc.ctx, role) { - return ErrNoPermissions.withStack() - } + raProps = &roleActionProps{ + role: &types.Role{ID: roleID}, + target: &types.Role{ID: targetRoleID}, + } + ) - return svc.role.ArchiveByID(roleID) -} - -func (svc role) Unarchive(roleID uint64) error { - role, err := svc.findByID(roleID) - if err != nil { - return err - } - - if !svc.ac.CanUpdateRole(svc.ctx, role) { - return ErrNoPermissions.withStack() - } - - return svc.role.UnarchiveByID(roleID) -} - -func (svc role) Merge(roleID, targetRoleID uint64) error { - role, err := svc.findByID(roleID) - if err != nil { - return err - } - - if targetRoleID == 0 { - return ErrInvalidID.withStack() - } - - if !svc.ac.CanUpdateRole(svc.ctx, role) { - return ErrNoPermissions.withStack() - } - - return svc.role.MergeByID(roleID, targetRoleID) + err = svc.db.Transaction(func() (err error) { + if roleID == 0 || targetRoleID == 0 { + return RoleErrInvalidID() + } + + if r, err = svc.findByID(roleID); err != nil { + return err + } + + raProps.setRole(r) + + if !svc.ac.CanUpdateRole(svc.ctx, r) { + return RoleErrNotAllowedToUpdate() + } + + if t, err = svc.findByID(targetRoleID); err != nil { + return err + } + + raProps.setTarget(t) + + if !svc.ac.CanUpdateRole(svc.ctx, t) { + return RoleErrNotAllowedToUpdate() + } + + if err = svc.role.MergeByID(roleID, targetRoleID); err != nil { + return + } + + return nil + }) + + return svc.recordAction(svc.ctx, raProps, RoleActionMerge, err) } +// Move +// +// @obsolete func (svc role) Move(roleID, targetOrganisationID uint64) error { - role, err := svc.findByID(roleID) - if err != nil { - return err - } - - if targetOrganisationID == 0 { - return ErrInvalidID.withStack() - } - - if !svc.ac.CanUpdateRole(svc.ctx, role) { - return ErrNoPermissions.withStack() - } - - return svc.role.MoveByID(roleID, targetOrganisationID) + return RoleErrGeneric().Wrap(fmt.Errorf("obsolete")) } func (svc role) Membership(userID uint64) ([]*types.RoleMember, error) { return svc.role.MembershipsFindByUserID(userID) } -func (svc role) MemberList(roleID uint64) ([]*types.RoleMember, error) { - if roleID == permissions.EveryoneRoleID { - return nil, ErrInvalidID.withStack() - } - - _, err := svc.findByID(roleID) - - if err != nil { - return nil, err - } - - return svc.role.MemberFindByRoleID(roleID) -} - -func (svc role) MemberAdd(roleID, userID uint64) (err error) { +func (svc role) MemberList(roleID uint64) (mm []*types.RoleMember, err error) { var ( - role *types.Role - user *types.User + r *types.Role + + raProps = &roleActionProps{ + role: &types.Role{ID: roleID}, + } ) - if role, err = svc.findByID(roleID); err != nil { - return - } + err = svc.db.Transaction(func() error { + if roleID == permissions.EveryoneRoleID || roleID == 0 { + return RoleErrInvalidID() + } - if user, err = svc.user.FindByID(userID); err != nil { - return - } + if r, err = svc.findByID(roleID); err != nil { + return err + } - if err = svc.eventbus.WaitFor(svc.ctx, event.RoleMemberBeforeAdd(user, role)); err != nil { - return - } + if !svc.ac.CanReadRole(svc.ctx, r) { + return RoleErrNotAllowedToRead() + } - if !svc.ac.CanManageRoleMembers(svc.ctx, role) { - return errors.New("Not allowed to manage role members") - } + if mm, err = svc.role.MemberFindByRoleID(roleID); err != nil { + return err + } - if err = svc.role.MemberAddByID(role.ID, user.ID); err != nil { - return - } + return nil + }) - defer svc.eventbus.Dispatch(svc.ctx, event.RoleMemberAfterAdd(user, role)) - return nil + return mm, svc.recordAction(svc.ctx, raProps, RoleActionMembers, err) } -func (svc role) MemberRemove(roleID, userID uint64) (err error) { +// MemberAdd adds member (user) to a role +func (svc role) MemberAdd(roleID, memberID uint64) (err error) { var ( - role *types.Role - user *types.User + r *types.Role + m *types.User + + raProps = &roleActionProps{ + role: &types.Role{ID: roleID}, + member: &types.User{ID: memberID}, + } ) - if role, err = svc.findByID(roleID); err != nil { - return - } + err = svc.db.Transaction(func() (err error) { + if roleID == permissions.EveryoneRoleID || roleID == 0 || memberID == 0 { + return RoleErrInvalidID() + } - if user, err = svc.user.FindByID(userID); err != nil { - return - } + if r, err = svc.findByID(roleID); err != nil { + return + } - if err = svc.eventbus.WaitFor(svc.ctx, event.RoleMemberBeforeRemove(user, role)); err != nil { - return - } + raProps.setRole(r) - if !svc.ac.CanManageRoleMembers(svc.ctx, role) { - return errors.New("Not allowed to manage role members") - } + if m, err = svc.user.FindByID(memberID); err != nil { + return + } - if err = svc.role.MemberRemoveByID(role.ID, user.ID); err != nil { - return - } + raProps.setMember(m) - defer svc.eventbus.Dispatch(svc.ctx, event.RoleMemberAfterRemove(user, role)) - return nil + if err = svc.eventbus.WaitFor(svc.ctx, event.RoleMemberBeforeAdd(m, r)); err != nil { + return + } + + if !svc.ac.CanManageRoleMembers(svc.ctx, r) { + return RoleErrNotAllowedToManageMembers() + } + + if err = svc.role.MemberAddByID(r.ID, m.ID); err != nil { + return + } + + defer svc.eventbus.Dispatch(svc.ctx, event.RoleMemberAfterAdd(m, r)) + return nil + }) + + return svc.recordAction(svc.ctx, raProps, RoleActionMemberAdd, err) +} + +// MemberRemove removes member (user) from a role +func (svc role) MemberRemove(roleID, memberID uint64) (err error) { + var ( + r *types.Role + m *types.User + raProps = &roleActionProps{ + role: &types.Role{ID: roleID}, + member: &types.User{ID: memberID}, + } + ) + + err = svc.db.Transaction(func() (err error) { + if roleID == permissions.EveryoneRoleID || roleID == 0 || memberID == 0 { + return RoleErrInvalidID() + } + + if r, err = svc.findByID(roleID); err != nil { + return + } + + raProps.setRole(r) + + if m, err = svc.user.FindByID(memberID); err != nil { + return + } + + raProps.setMember(m) + + if err = svc.eventbus.WaitFor(svc.ctx, event.RoleMemberBeforeRemove(m, r)); err != nil { + return + } + + if !svc.ac.CanManageRoleMembers(svc.ctx, r) { + return RoleErrNotAllowedToManageMembers() + } + + if err = svc.role.MemberRemoveByID(r.ID, m.ID); err != nil { + return + } + + defer svc.eventbus.Dispatch(svc.ctx, event.RoleMemberAfterRemove(m, r)) + return nil + }) + + return svc.recordAction(svc.ctx, raProps, RoleActionMemberRemove, err) } var _ RoleService = &role{} diff --git a/system/service/role_actions.gen.go b/system/service/role_actions.gen.go new file mode 100644 index 000000000..b32469ee3 --- /dev/null +++ b/system/service/role_actions.gen.go @@ -0,0 +1,1171 @@ +package service + +// This file is auto-generated from system/service/role_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + roleActionProps struct { + member *types.User + role *types.Role + new *types.Role + update *types.Role + existing *types.Role + target *types.Role + filter *types.RoleFilter + } + + roleAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *roleActionProps + } + + roleError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *roleActionProps + } +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setMember updates roleActionProps's member +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *roleActionProps) setMember(member *types.User) *roleActionProps { + p.member = member + return p +} + +// setRole updates roleActionProps's role +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *roleActionProps) setRole(role *types.Role) *roleActionProps { + p.role = role + return p +} + +// setNew updates roleActionProps's new +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *roleActionProps) setNew(new *types.Role) *roleActionProps { + p.new = new + return p +} + +// setUpdate updates roleActionProps's update +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *roleActionProps) setUpdate(update *types.Role) *roleActionProps { + p.update = update + return p +} + +// setExisting updates roleActionProps's existing +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *roleActionProps) setExisting(existing *types.Role) *roleActionProps { + p.existing = existing + return p +} + +// setTarget updates roleActionProps's target +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *roleActionProps) setTarget(target *types.Role) *roleActionProps { + p.target = target + return p +} + +// setFilter updates roleActionProps's filter +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *roleActionProps) setFilter(filter *types.RoleFilter) *roleActionProps { + p.filter = filter + return p +} + +// serialize converts roleActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p roleActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + str = func(i interface{}) string { return fmt.Sprintf("%v", i) } + ) + + if p.member != nil { + m["member.handle"] = str(p.member.Handle) + m["member.email"] = str(p.member.Email) + m["member.name"] = str(p.member.Name) + m["member.ID"] = str(p.member.ID) + } + if p.role != nil { + m["role.handle"] = str(p.role.Handle) + m["role.name"] = str(p.role.Name) + m["role.ID"] = str(p.role.ID) + } + if p.new != nil { + m["new.handle"] = str(p.new.Handle) + m["new.name"] = str(p.new.Name) + m["new.ID"] = str(p.new.ID) + } + if p.update != nil { + m["update.handle"] = str(p.update.Handle) + m["update.name"] = str(p.update.Name) + m["update.ID"] = str(p.update.ID) + } + if p.existing != nil { + m["existing.handle"] = str(p.existing.Handle) + m["existing.name"] = str(p.existing.Name) + m["existing.ID"] = str(p.existing.ID) + } + if p.target != nil { + m["target.handle"] = str(p.target.Handle) + m["target.name"] = str(p.target.Name) + m["target.ID"] = str(p.target.ID) + } + if p.filter != nil { + m["filter.query"] = str(p.filter.Query) + m["filter.roleID"] = str(p.filter.RoleID) + m["filter.memberID"] = str(p.filter.MemberID) + m["filter.handle"] = str(p.filter.Handle) + m["filter.name"] = str(p.filter.Name) + m["filter.deleted"] = str(p.filter.Deleted) + m["filter.archived"] = str(p.filter.Archived) + m["filter.sort"] = str(p.filter.Sort) + } + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p roleActionProps) tr(in string, err error) string { + var pairs = []string{"{err}"} + + if err != nil { + for { + // Unwrap errors + ue := errors.Unwrap(err) + if ue == nil { + break + } + + err = ue + } + + pairs = append(pairs, err.Error()) + } else { + pairs = append(pairs, "nil") + } + + if p.member != nil { + pairs = append(pairs, "{member}", fmt.Sprintf("%v", p.member.Handle)) + pairs = append(pairs, "{member.handle}", fmt.Sprintf("%v", p.member.Handle)) + pairs = append(pairs, "{member.email}", fmt.Sprintf("%v", p.member.Email)) + pairs = append(pairs, "{member.name}", fmt.Sprintf("%v", p.member.Name)) + pairs = append(pairs, "{member.ID}", fmt.Sprintf("%v", p.member.ID)) + } + + if p.role != nil { + pairs = append(pairs, "{role}", fmt.Sprintf("%v", p.role.Handle)) + pairs = append(pairs, "{role.handle}", fmt.Sprintf("%v", p.role.Handle)) + pairs = append(pairs, "{role.name}", fmt.Sprintf("%v", p.role.Name)) + pairs = append(pairs, "{role.ID}", fmt.Sprintf("%v", p.role.ID)) + } + + if p.new != nil { + pairs = append(pairs, "{new}", fmt.Sprintf("%v", p.new.Handle)) + pairs = append(pairs, "{new.handle}", fmt.Sprintf("%v", p.new.Handle)) + pairs = append(pairs, "{new.name}", fmt.Sprintf("%v", p.new.Name)) + pairs = append(pairs, "{new.ID}", fmt.Sprintf("%v", p.new.ID)) + } + + if p.update != nil { + pairs = append(pairs, "{update}", fmt.Sprintf("%v", p.update.Handle)) + pairs = append(pairs, "{update.handle}", fmt.Sprintf("%v", p.update.Handle)) + pairs = append(pairs, "{update.name}", fmt.Sprintf("%v", p.update.Name)) + pairs = append(pairs, "{update.ID}", fmt.Sprintf("%v", p.update.ID)) + } + + if p.existing != nil { + pairs = append(pairs, "{existing}", fmt.Sprintf("%v", p.existing.Handle)) + pairs = append(pairs, "{existing.handle}", fmt.Sprintf("%v", p.existing.Handle)) + pairs = append(pairs, "{existing.name}", fmt.Sprintf("%v", p.existing.Name)) + pairs = append(pairs, "{existing.ID}", fmt.Sprintf("%v", p.existing.ID)) + } + + if p.target != nil { + pairs = append(pairs, "{target}", fmt.Sprintf("%v", p.target.Handle)) + pairs = append(pairs, "{target.handle}", fmt.Sprintf("%v", p.target.Handle)) + pairs = append(pairs, "{target.name}", fmt.Sprintf("%v", p.target.Name)) + pairs = append(pairs, "{target.ID}", fmt.Sprintf("%v", p.target.ID)) + } + + if p.filter != nil { + pairs = append(pairs, "{filter}", fmt.Sprintf("%v", p.filter.Query)) + pairs = append(pairs, "{filter.query}", fmt.Sprintf("%v", p.filter.Query)) + pairs = append(pairs, "{filter.roleID}", fmt.Sprintf("%v", p.filter.RoleID)) + pairs = append(pairs, "{filter.memberID}", fmt.Sprintf("%v", p.filter.MemberID)) + pairs = append(pairs, "{filter.handle}", fmt.Sprintf("%v", p.filter.Handle)) + pairs = append(pairs, "{filter.name}", fmt.Sprintf("%v", p.filter.Name)) + pairs = append(pairs, "{filter.deleted}", fmt.Sprintf("%v", p.filter.Deleted)) + pairs = append(pairs, "{filter.archived}", fmt.Sprintf("%v", p.filter.Archived)) + pairs = append(pairs, "{filter.sort}", fmt.Sprintf("%v", p.filter.Sort)) + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *roleAction) String() string { + var props = &roleActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *roleAction) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error methods + +// String returns loggable description as string +// +// It falls back to message if log is not set +// +// This function is auto-generated. +// +func (e *roleError) String() string { + var props = &roleActionProps{} + + if e.props != nil { + props = e.props + } + + if e.wrap != nil && !strings.Contains(e.log, "{err}") { + // Suffix error log with {err} to ensure + // we log the cause for this error + e.log += ": {err}" + } + + return props.tr(e.log, e.wrap) +} + +// Error satisfies +// +// This function is auto-generated. +// +func (e *roleError) Error() string { + var props = &roleActionProps{} + + if e.props != nil { + props = e.props + } + + return props.tr(e.message, e.wrap) +} + +// Is fn for error equality check +// +// This function is auto-generated. +// +func (e *roleError) Is(Resource error) bool { + t, ok := Resource.(*roleError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps roleError around another error +// +// This function is auto-generated. +// +func (e *roleError) Wrap(err error) *roleError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *roleError) Unwrap() error { + return e.wrap +} + +func (e *roleError) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Error: e.Error(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action constructors + +// RoleActionSearch returns "system:role.search" error +// +// This function is auto-generated. +// +func RoleActionSearch(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "search", + log: "searched for roles", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionLookup returns "system:role.lookup" error +// +// This function is auto-generated. +// +func RoleActionLookup(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "lookup", + log: "looked-up for a {role}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionCreate returns "system:role.create" error +// +// This function is auto-generated. +// +func RoleActionCreate(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "create", + log: "created {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionUpdate returns "system:role.update" error +// +// This function is auto-generated. +// +func RoleActionUpdate(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "update", + log: "updated {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionDelete returns "system:role.delete" error +// +// This function is auto-generated. +// +func RoleActionDelete(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "delete", + log: "deleted {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionUndelete returns "system:role.undelete" error +// +// This function is auto-generated. +// +func RoleActionUndelete(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "undelete", + log: "undeleted {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionArchive returns "system:role.archive" error +// +// This function is auto-generated. +// +func RoleActionArchive(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "archive", + log: "archived {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionUnarchive returns "system:role.unarchive" error +// +// This function is auto-generated. +// +func RoleActionUnarchive(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "unarchive", + log: "unarchived {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionMerge returns "system:role.merge" error +// +// This function is auto-generated. +// +func RoleActionMerge(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "merge", + log: "merged {target} with {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionMembers returns "system:role.members" error +// +// This function is auto-generated. +// +func RoleActionMembers(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "members", + log: "searched for members of {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionMemberAdd returns "system:role.memberAdd" error +// +// This function is auto-generated. +// +func RoleActionMemberAdd(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "memberAdd", + log: "added {member.email} to {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// RoleActionMemberRemove returns "system:role.memberRemove" error +// +// This function is auto-generated. +// +func RoleActionMemberRemove(props ...*roleActionProps) *roleAction { + a := &roleAction{ + timestamp: time.Now(), + resource: "system:role", + action: "memberRemove", + log: "removed {member.email} from {role}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// RoleErrGeneric returns "system:role.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func RoleErrGeneric(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNonexistent returns "system:role.nonexistent" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func RoleErrNonexistent(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "nonexistent", + action: "error", + message: "role does not exist", + log: "role does not exist", + severity: actionlog.Warning, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrInvalidID returns "system:role.invalidID" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func RoleErrInvalidID(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "invalidID", + action: "error", + message: "invalid ID", + log: "invalid ID", + severity: actionlog.Warning, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrInvalidHandle returns "system:role.invalidHandle" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func RoleErrInvalidHandle(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "invalidHandle", + action: "error", + message: "invalid handle", + log: "invalid handle", + severity: actionlog.Warning, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToRead returns "system:role.notAllowedToRead" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToRead(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToRead", + action: "error", + message: "not allowed to read role", + log: "failed to read {role.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToListRoles returns "system:role.notAllowedToListRoles" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToListRoles(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToListRoles", + action: "error", + message: "not allowed to list roles", + log: "failed to list role; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToCreate returns "system:role.notAllowedToCreate" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToCreate(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToCreate", + action: "error", + message: "not allowed to create role", + log: "failed to create role; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToUpdate returns "system:role.notAllowedToUpdate" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToUpdate(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToUpdate", + action: "error", + message: "not allowed to update role", + log: "failed to update {role.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToDelete returns "system:role.notAllowedToDelete" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToDelete(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToDelete", + action: "error", + message: "not allowed to delete role", + log: "failed to delete {role.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToUndelete returns "system:role.notAllowedToUndelete" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToUndelete(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToUndelete", + action: "error", + message: "not allowed to undelete role", + log: "failed to undelete {role.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToArchive returns "system:role.notAllowedToArchive" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToArchive(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToArchive", + action: "error", + message: "not allowed to archive role", + log: "failed to archive {role.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToUnarchive returns "system:role.notAllowedToUnarchive" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToUnarchive(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToUnarchive", + action: "error", + message: "not allowed to unarchive role", + log: "failed to unarchive {role.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNotAllowedToManageMembers returns "system:role.notAllowedToManageMembers" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func RoleErrNotAllowedToManageMembers(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "notAllowedToManageMembers", + action: "error", + message: "not allowed to manage role members", + log: "failed to manage {role.handle} members; insufficient permissions", + severity: actionlog.Alert, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrHandleNotUnique returns "system:role.handleNotUnique" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func RoleErrHandleNotUnique(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "handleNotUnique", + action: "error", + message: "role handle not unique", + log: "used duplicate handle ({role.handle}) for role", + severity: actionlog.Warning, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// RoleErrNameNotUnique returns "system:role.nameNotUnique" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func RoleErrNameNotUnique(props ...*roleActionProps) *roleError { + var e = &roleError{ + timestamp: time.Now(), + resource: "system:role", + error: "nameNotUnique", + action: "error", + message: "role name not unique", + log: "used duplicate name ({role.name}) for role", + severity: actionlog.Warning, + props: func() *roleActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* + +// recordAction is a service helper function wraps function that can return error +// +// context is used to enrich audit log entry with current user info, request ID, IP address... +// props are collected action/error properties +// action (optional) fn will be used to construct roleAction struct from given props (and error) +// err is any error that occurred while action was happening +// +// Action has success and fail (error) state: +// - when recorded without an error (4th param), action is recorded as successful. +// - when an additional error is given (4th param), action is used to wrap +// the additional error +// +// This function is auto-generated. +// +func (svc role) recordAction(ctx context.Context, props *roleActionProps, action func(...*roleActionProps) *roleAction, err error) error { + var ( + ok bool + + // Return error + retError *roleError + + // Recorder error + recError *roleError + ) + + if err != nil { + if retError, ok = err.(*roleError); !ok { + // got non-role error, wrap it with RoleErrGeneric + retError = RoleErrGeneric(props).Wrap(err) + + // copy action to returning and recording error + retError.action = action().action + + // we'll use RoleErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + // copy action to returning and recording error + retError.action = action().action + // start with copy of return error for recording + // this will be updated with tha root cause as we try and + // unwrap the error + recError = retError + + // find the original recError for this error + // for the purpose of logging + var unwrappedError error = retError + for { + if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil { + // nothing wrapped + break + } + + // update recError ONLY of wrapped error is of type roleError + if unwrappedSinkError, ok := unwrappedError.(*roleError); ok { + recError = unwrappedSinkError + } + } + + if retError.props == nil { + // set props on returning error if empty + retError.props = props + } + + if recError.props == nil { + // set props on recording error if empty + recError.props = props + } + } + } + + if svc.actionlog != nil { + if retError != nil { + // failed action, log error + svc.actionlog.Record(ctx, recError) + } else if action != nil { + // successful + svc.actionlog.Record(ctx, action(props)) + } + } + + if err == nil { + // retError not an interface and that WILL (!!) cause issues + // with nil check (== nil) when it is not explicitly returned + return nil + } + + return retError +} diff --git a/system/service/role_actions.yaml b/system/service/role_actions.yaml new file mode 100644 index 000000000..d1be48009 --- /dev/null +++ b/system/service/role_actions.yaml @@ -0,0 +1,135 @@ +# List of loggable service actions + +resource: system:role +service: role + +# Default sensitivity for actions +defaultActionSeverity: notice + +# default severity for errors +defaultErrorSeverity: alert + +import: + - github.com/cortezaproject/corteza-server/system/types + +props: + - name: member + type: "*types.User" + fields: [ handle, email, name, ID ] + - name: role + type: "*types.Role" + fields: [ handle, name, ID ] + - name: new + type: "*types.Role" + fields: [ handle, name, ID ] + - name: update + type: "*types.Role" + fields: [ handle, name, ID ] + - name: existing + type: "*types.Role" + fields: [ handle, name, ID ] + - name: target + type: "*types.Role" + fields: [ handle, name, ID ] + - name: filter + type: "*types.RoleFilter" + fields: [ query, roleID, memberID, handle, name, deleted, archived, sort ] + +actions: + - action: search + log: "searched for roles" + severity: info + + - action: lookup + log: "looked-up for a {role}" + severity: info + + - action: create + log: "created {role}" + + - action: update + log: "updated {role}" + + - action: delete + log: "deleted {role}" + + - action: undelete + log: "undeleted {role}" + + - action: archive + log: "archived {role}" + + - action: unarchive + log: "unarchived {role}" + + - action: merge + log: "merged {target} with {role}" + + - action: members + log: "searched for members of {role}" + + - action: memberAdd + log: "added {member.email} to {role}" + + - action: memberRemove + log: "removed {member.email} from {role}" + + +errors: + - error: nonexistent + message: "role does not exist" + severity: warning + + - error: invalidID + message: "invalid ID" + severity: warning + + - error: invalidHandle + message: "invalid handle" + severity: warning + + - error: notAllowedToRead + message: "not allowed to read role" + log: "failed to read {role.handle}; insufficient permissions" + + - error: notAllowedToListRoles + message: "not allowed to list roles" + log: "failed to list role; insufficient permissions" + + - error: notAllowedToCreate + message: "not allowed to create role" + log: "failed to create role; insufficient permissions" + + - error: notAllowedToUpdate + message: "not allowed to update role" + log: "failed to update {role.handle}; insufficient permissions" + + - error: notAllowedToDelete + message: "not allowed to delete role" + log: "failed to delete {role.handle}; insufficient permissions" + + - error: notAllowedToUndelete + message: "not allowed to undelete role" + log: "failed to undelete {role.handle}; insufficient permissions" + + - error: notAllowedToArchive + message: "not allowed to archive role" + log: "failed to archive {role.handle}; insufficient permissions" + + - error: notAllowedToUnarchive + message: "not allowed to unarchive role" + log: "failed to unarchive {role.handle}; insufficient permissions" + + - error: notAllowedToManageMembers + message: "not allowed to manage role members" + log: "failed to manage {role.handle} members; insufficient permissions" + + - error: handleNotUnique + message: "role handle not unique" + log: "used duplicate handle ({role.handle}) for role" + severity: warning + + - error: nameNotUnique + message: "role name not unique" + log: "used duplicate name ({role.name}) for role" + severity: warning diff --git a/system/service/service.go b/system/service/service.go index b8f16668f..65214087f 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -2,17 +2,19 @@ package service import ( "context" - "github.com/cortezaproject/corteza-server/pkg/store" - "github.com/cortezaproject/corteza-server/pkg/store/minio" - "github.com/cortezaproject/corteza-server/pkg/store/plain" "go.uber.org/zap" + "github.com/cortezaproject/corteza-server/pkg/actionlog" + actionlogRepository "github.com/cortezaproject/corteza-server/pkg/actionlog/repository" "github.com/cortezaproject/corteza-server/pkg/app/options" intAuth "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/permissions" "github.com/cortezaproject/corteza-server/pkg/settings" + "github.com/cortezaproject/corteza-server/pkg/store" + "github.com/cortezaproject/corteza-server/pkg/store/minio" + "github.com/cortezaproject/corteza-server/pkg/store/plain" "github.com/cortezaproject/corteza-server/system/repository" "github.com/cortezaproject/corteza-server/system/types" ) @@ -74,6 +76,8 @@ var ( // CurrentSettings represents current system settings CurrentSettings = &types.Settings{} + DefaultActionlog actionlog.Recorder + DefaultSink *sink DefaultAuth AuthService @@ -90,6 +94,12 @@ var ( func Initialize(ctx context.Context, log *zap.Logger, c Config) (err error) { DefaultLogger = log.Named("service") + DefaultActionlog = actionlog.NewService( + actionlogRepository.Mysql(repository.DB(ctx), "sys_actionlog"), + log, + log, + ) + if DefaultPermissions == nil { // Do not override permissions service stored under DefaultPermissions // to allow integration tests to inject own permission service diff --git a/system/service/user.go b/system/service/user.go index a1dd0ec2a..dd2954a9b 100644 --- a/system/service/user.go +++ b/system/service/user.go @@ -2,45 +2,36 @@ package service import ( "context" - "github.com/cortezaproject/corteza-server/pkg/handle" - "github.com/cortezaproject/corteza-server/pkg/rh" "io" "net/mail" "regexp" "strconv" "strings" - "github.com/pkg/errors" "github.com/titpetric/factory" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" + "github.com/cortezaproject/corteza-server/pkg/actionlog" internalAuth "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/eventbus" - "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/cortezaproject/corteza-server/pkg/permissions" + "github.com/cortezaproject/corteza-server/pkg/rh" "github.com/cortezaproject/corteza-server/system/repository" "github.com/cortezaproject/corteza-server/system/service/event" "github.com/cortezaproject/corteza-server/system/types" ) const ( - ErrUserInvalidCredentials = serviceError("UserInvalidCredentials") - ErrUserHandleNotUnique = serviceError("UserHandleNotUnique") - ErrUserUsernameNotUnique = serviceError("UserUsernameNotUnique") - ErrUserEmailNotUnique = serviceError("UserEmailNotUnique") - ErrUserInvalidEmail = serviceError("UserInvalidEmail") - ErrUserLocked = serviceError("UserLocked") - maskPrivateDataEmail = "####.#######@######.###" maskPrivateDataName = "##### ##########" ) type ( user struct { - db *factory.DB - ctx context.Context - logger *zap.Logger + db *factory.DB + ctx context.Context + + actionlog actionlog.Recorder settings *types.Settings @@ -56,7 +47,7 @@ type ( } userAuth interface { - checkPasswordStrength(string) error + checkPasswordStrength(string) bool changePassword(uint64, string) error } @@ -79,6 +70,10 @@ type ( FilterUsersWithUnmaskableName(ctx context.Context) *permissions.ResourceFilter } + // Temp types to support user.Preloader + userIdGetter func(chan uint64) + userSetter func(*types.User) error + UserService interface { With(ctx context.Context) UserService @@ -101,33 +96,32 @@ type ( Undelete(id uint64) error SetPassword(userID uint64, password string) error + + Preloader(userIdGetter, types.UserFilter, userSetter) error } ) func User(ctx context.Context) UserService { return (&user{ - logger: DefaultLogger.Named("user"), eventbus: eventbus.Service(), ac: DefaultAccessControl, settings: CurrentSettings, auth: DefaultAuth, + actionlog: DefaultActionlog, + subscription: CurrentSubscription, }).With(ctx) } -// log() returns zap's logger with requestID from current context and fields. -func (svc user) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(ctx, svc.logger).With(fields...) -} - func (svc user) With(ctx context.Context) UserService { db := repository.DB(ctx) return &user{ - ctx: ctx, - db: db, - logger: svc.logger, + ctx: ctx, + db: db, + + actionlog: svc.actionlog, ac: svc.ac, eventbus: svc.eventbus, @@ -141,33 +135,70 @@ func (svc user) With(ctx context.Context) UserService { } } -func (svc user) FindByID(ID uint64) (*types.User, error) { - if ID == 0 { - return nil, ErrInvalidID.withStack() - } +func (svc user) FindByID(userID uint64) (u *types.User, err error) { + var ( + uaProps = &userActionProps{user: &types.User{ID: userID}} + ) - tmp := internalAuth.NewIdentity(ID) - if internalAuth.IsSuperUser(tmp) { - // Handling case when looking for a super-user - // - // Currently, superuser is a virtual entity - // and does not exists in the db - return &types.User{ID: ID}, nil - } + err = svc.db.Transaction(func() error { + if userID == 0 { + return UserErrInvalidID() + } - return svc.proc(svc.user.FindByID(ID)) + su := internalAuth.NewIdentity(userID) + if internalAuth.IsSuperUser(su) { + // Handling case when looking for a super-user + // + // Currently, superuser is a virtual entity + // and does not exists in the db + u = &types.User{ID: userID} + return nil + } + + u, err = svc.proc(svc.user.FindByID(userID)) + return err + }) + + return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err) } -func (svc user) FindByEmail(email string) (*types.User, error) { - return svc.proc(svc.user.FindByEmail(email)) +func (svc user) FindByEmail(email string) (u *types.User, err error) { + var ( + uaProps = &userActionProps{user: &types.User{Email: email}} + ) + + err = svc.db.Transaction(func() error { + u, err = svc.proc(svc.user.FindByEmail(email)) + return err + }) + + return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err) } -func (svc user) FindByUsername(username string) (*types.User, error) { - return svc.proc(svc.user.FindByUsername(username)) +func (svc user) FindByUsername(username string) (u *types.User, err error) { + var ( + uaProps = &userActionProps{user: &types.User{Username: username}} + ) + + err = svc.db.Transaction(func() error { + u, err = svc.proc(svc.user.FindByUsername(username)) + return err + }) + + return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err) } -func (svc user) FindByHandle(handle string) (*types.User, error) { - return svc.proc(svc.user.FindByHandle(handle)) +func (svc user) FindByHandle(handle string) (u *types.User, err error) { + var ( + uaProps = &userActionProps{user: &types.User{Handle: handle}} + ) + + err = svc.db.Transaction(func() error { + u, err = svc.proc(svc.user.FindByHandle(handle)) + return err + }) + + return u, svc.recordAction(svc.ctx, uaProps, UserActionLookup, err) } // FindByAny finds user by given identifier (context, id, handle, email) @@ -189,7 +220,7 @@ func (svc user) FindByAny(identifier interface{}) (u *types.User, err error) { u, err = svc.FindByHandle(strIdentifier) } } else { - err = ErrInvalidID.withStack() + err = UserErrInvalidID() } if err != nil { @@ -215,74 +246,80 @@ func (svc user) proc(u *types.User, err error) (*types.User, error) { return u, nil } -func (svc user) Find(f types.UserFilter) (types.UserSet, types.UserFilter, error) { - if f.Deleted > 0 { - // If list with deleted users is requested - // user must have access permissions to system (ie: is admin) - // - // not the best solution but ATM it allows us to have at least - // some kind of control over who can see deleted users - if !svc.ac.CanAccess(svc.ctx) { - return nil, f, ErrNoPermissions.withStack() +func (svc user) Find(filter types.UserFilter) (uu types.UserSet, f types.UserFilter, err error) { + var ( + uaProps = &userActionProps{filter: &filter} + ) + + err = svc.db.Transaction(func() error { + if filter.Deleted > 0 { + // If list with deleted users is requested + // user must have access permissions to system (ie: is admin) + // + // not the best solution but ATM it allows us to have at least + // some kind of control over who can see deleted users + if !svc.ac.CanAccess(svc.ctx) { + return UserErrNotAllowedToListUsers() + } } - } - // Prepare filter for email unmasking check - f.IsEmailUnmaskable = svc.ac.FilterUsersWithUnmaskableEmail(svc.ctx) + // Prepare filter for email unmasking check + filter.IsEmailUnmaskable = svc.ac.FilterUsersWithUnmaskableEmail(svc.ctx) - // Prepare filter for name unmasking check - f.IsNameUnmaskable = svc.ac.FilterUsersWithUnmaskableName(svc.ctx) + // Prepare filter for name unmasking check + filter.IsNameUnmaskable = svc.ac.FilterUsersWithUnmaskableName(svc.ctx) - f.IsReadable = svc.ac.FilterReadableUsers(svc.ctx) + filter.IsReadable = svc.ac.FilterReadableUsers(svc.ctx) - return svc.procSet(svc.user.Find(f)) -} + uu, f, err = svc.user.Find(filter) + if err != nil { + return err + } -func (svc user) procSet(u types.UserSet, f types.UserFilter, err error) (types.UserSet, types.UserFilter, error) { - if err != nil { - return nil, f, err - } - - _ = u.Walk(func(u *types.User) error { - svc.handlePrivateData(u) - return nil + return uu.Walk(func(u *types.User) error { + svc.handlePrivateData(u) + return nil + }) }) - return u, f, nil + return uu, f, svc.recordAction(svc.ctx, uaProps, UserActionSearch, err) } func (svc user) Create(new *types.User) (u *types.User, err error) { - if !svc.ac.CanCreateUser(svc.ctx) { - return nil, ErrNoCreatePermissions.withStack() - } + var ( + uaProps = &userActionProps{new: new} + ) - if !handle.IsValid(new.Handle) { - return nil, ErrInvalidHandle.withStack() - } - - if _, err := mail.ParseAddress(new.Email); err != nil { - return nil, ErrUserInvalidEmail.withStack() - } - - if svc.subscription != nil { - // When we have an active subscription, we need to check - // if users can be creare or did this deployment hit - // it's user-limit - err = svc.subscription.CanCreateUser(svc.user.Total()) - if err != nil { - return nil, err + err = svc.db.Transaction(func() (err error) { + if !svc.ac.CanCreateUser(svc.ctx) { + return UserErrNotAllowedToCreate() } - } - if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeCreate(new, u)); err != nil { - return - } + if !handle.IsValid(new.Handle) { + return UserErrInvalidHandle() + } - if new.Handle == "" { - createHandle(svc.user, new) - } + if _, err := mail.ParseAddress(new.Email); err != nil { + return UserErrInvalidEmail() + } - return u, svc.db.Transaction(func() (err error) { + if svc.subscription != nil { + // When we have an active subscription, we need to check + // if users can be create or did this deployment hit + // it's user-limit + err = svc.subscription.CanCreateUser(svc.user.Total()) + if err != nil { + return err + } + } + + if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeCreate(new, u)); err != nil { + return + } + + if new.Handle == "" { + createHandle(svc.user, new) + } if err = svc.UniqueCheck(new); err != nil { return @@ -295,6 +332,8 @@ func (svc user) Create(new *types.User) (u *types.User, err error) { defer svc.eventbus.Dispatch(svc.ctx, event.UserAfterCreate(new, u)) return }) + + return u, svc.recordAction(svc.ctx, uaProps, UserActionCreate, err) } func (svc user) CreateWithAvatar(input *types.User, avatar io.Reader) (out *types.User, err error) { @@ -303,40 +342,46 @@ func (svc user) CreateWithAvatar(input *types.User, avatar io.Reader) (out *type } func (svc user) Update(upd *types.User) (u *types.User, err error) { - if upd.ID == 0 { - return nil, ErrInvalidID.withStack() - } + var ( + uaProps = &userActionProps{update: upd} + ) - if !handle.IsValid(upd.Handle) { - return nil, ErrInvalidHandle.withStack() - } - - if _, err := mail.ParseAddress(upd.Email); err != nil { - return nil, ErrUserInvalidEmail.withStack() - } - - if u, err = svc.user.FindByID(upd.ID); err != nil { - return - } - - if upd.ID != internalAuth.GetIdentityFromContext(svc.ctx).Identity() { - if !svc.ac.CanUpdateUser(svc.ctx, u) { - return nil, ErrNoUpdatePermissions.withStack() + err = svc.db.Transaction(func() (err error) { + if upd.ID == 0 { + return ErrInvalidID } - } - // Assign changed values - u.Email = upd.Email - u.Username = upd.Username - u.Name = upd.Name - u.Handle = upd.Handle - u.Kind = upd.Kind + if !handle.IsValid(upd.Handle) { + return UserErrInvalidHandle() + } - if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeUpdate(upd, u)); err != nil { - return - } + if _, err := mail.ParseAddress(upd.Email); err != nil { + return UserErrInvalidEmail() + } + + if u, err = svc.user.FindByID(upd.ID); err != nil { + return + } + + uaProps.setUser(u) + + if upd.ID != internalAuth.GetIdentityFromContext(svc.ctx).Identity() { + if !svc.ac.CanUpdateUser(svc.ctx, u) { + return UserErrNotAllowedToUpdate() + } + } + + // Assign changed values + u.Email = upd.Email + u.Username = upd.Username + u.Name = upd.Name + u.Handle = upd.Handle + u.Kind = upd.Kind + + if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeUpdate(upd, u)); err != nil { + return + } - return u, svc.db.Transaction(func() (err error) { if err = svc.UniqueCheck(u); err != nil { return } @@ -348,8 +393,11 @@ func (svc user) Update(upd *types.User) (u *types.User, err error) { defer svc.eventbus.Dispatch(svc.ctx, event.UserAfterUpdate(upd, u)) return }) + + return u, svc.recordAction(svc.ctx, uaProps, UserActionUpdate, err) } +// UniqueCheck verifies user's email, username and handle func (svc user) UniqueCheck(u *types.User) (err error) { isUnique := func(field string) bool { f := types.UserFilter{ @@ -392,15 +440,15 @@ func (svc user) UniqueCheck(u *types.User) (err error) { } if !isUnique("email") { - return ErrUserEmailNotUnique + return UserErrEmailNotUnique() } if !isUnique("username") { - return ErrUserUsernameNotUnique + return UserErrUsernameNotUnique() } if !isUnique("handle") { - return ErrUserHandleNotUnique + return UserErrHandleNotUnique() } return nil @@ -411,128 +459,173 @@ func (svc user) UpdateWithAvatar(mod *types.User, avatar io.Reader) (out *types. return svc.Create(mod) } -func (svc user) Delete(ID uint64) (err error) { +func (svc user) Delete(userID uint64) (err error) { var ( - del *types.User + u *types.User + uaProps = &userActionProps{user: &types.User{ID: userID}} ) - if ID == 0 { - return ErrInvalidID.withStack() - } + err = svc.db.Transaction(func() (err error) { + if userID == 0 { + return UserErrInvalidID() + } - if del, err = svc.user.FindByID(ID); err != nil { - return - } + if u, err = svc.user.FindByID(userID); err != nil { + return + } - if !svc.ac.CanDeleteUser(svc.ctx, del) { - return ErrNoPermissions.withStack() - } + if !svc.ac.CanDeleteUser(svc.ctx, u) { + return UserErrNotAllowedToDelete() + } - if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeUpdate(nil, del)); err != nil { - return - } + if err = svc.eventbus.WaitFor(svc.ctx, event.UserBeforeUpdate(nil, u)); err != nil { + return + } - if err = svc.user.DeleteByID(ID); err != nil { - return - } + if err = svc.user.DeleteByID(userID); err != nil { + return + } - defer svc.eventbus.Dispatch(svc.ctx, event.UserAfterDelete(nil, del)) - return + defer svc.eventbus.Dispatch(svc.ctx, event.UserAfterDelete(nil, u)) + return nil + }) + + return svc.recordAction(svc.ctx, uaProps, UserActionDelete, err) } -func (svc user) Undelete(ID uint64) (err error) { - if ID == 0 { - return ErrInvalidID.withStack() - } +func (svc user) Undelete(userID uint64) (err error) { + var ( + u *types.User + uaProps = &userActionProps{user: &types.User{ID: userID}} + ) - var u *types.User - if u, err = svc.user.FindByID(ID); err != nil { - return - } + err = svc.db.Transaction(func() (err error) { + if userID == 0 { + return UserErrInvalidID() + } - if err = svc.UniqueCheck(u); err != nil { - return err - } + if u, err = svc.user.FindByID(userID); err != nil { + return + } - if !svc.ac.CanDeleteUser(svc.ctx, u) { - return ErrNoPermissions.withStack() - } + uaProps.setUser(u) - return svc.db.Transaction(func() (err error) { - return svc.user.UndeleteByID(ID) + if err = svc.UniqueCheck(u); err != nil { + return err + } + + if !svc.ac.CanDeleteUser(svc.ctx, u) { + return UserErrNotAllowedToDelete() + } + + if err = svc.user.UndeleteByID(userID); err != nil { + return err + } + + return nil }) + + return svc.recordAction(svc.ctx, uaProps, UserActionUndelete, err) + } -func (svc user) Suspend(ID uint64) (err error) { - if ID == 0 { - return ErrInvalidID.withStack() - } +func (svc user) Suspend(userID uint64) (err error) { + var ( + u *types.User + uaProps = &userActionProps{user: &types.User{ID: userID}} + ) - var u *types.User - if u, err = svc.user.FindByID(ID); err != nil { - return - } + err = svc.db.Transaction(func() (err error) { + if userID == 0 { + return UserErrInvalidID() + } - if !svc.ac.CanSuspendUser(svc.ctx, u) { - return ErrNoPermissions.withStack() - } + if u, err = svc.user.FindByID(userID); err != nil { + return + } - return svc.db.Transaction(func() (err error) { - return svc.user.SuspendByID(ID) + uaProps.setUser(u) + + if !svc.ac.CanSuspendUser(svc.ctx, u) { + return UserErrNotAllowedToSuspend() + } + + if err = svc.user.SuspendByID(userID); err != nil { + return err + } + + return nil }) + + return svc.recordAction(svc.ctx, uaProps, UserActionSuspend, err) + } -func (svc user) Unsuspend(ID uint64) (err error) { - if ID == 0 { - return ErrInvalidID.withStack() - } +func (svc user) Unsuspend(userID uint64) (err error) { + var ( + u *types.User + uaProps = &userActionProps{user: &types.User{ID: userID}} + ) - var u *types.User - if u, err = svc.user.FindByID(ID); err != nil { - return - } + err = svc.db.Transaction(func() (err error) { + if userID == 0 { + return UserErrInvalidID() + } - if !svc.ac.CanUnsuspendUser(svc.ctx, u) { - return ErrNoPermissions.withStack() - } + if u, err = svc.user.FindByID(userID); err != nil { + return + } - return svc.db.Transaction(func() (err error) { - return svc.user.UnsuspendByID(ID) + uaProps.setUser(u) + + if !svc.ac.CanUnsuspendUser(svc.ctx, u) { + return UserErrNotAllowedToUnsuspend() + } + + if err = svc.user.UnsuspendByID(userID); err != nil { + return err + } + + return nil }) + + return svc.recordAction(svc.ctx, uaProps, UserActionUnsuspend, err) + } // SetPassword sets new password for a user // // Expecting setter to have permissions to update modify users and internal authentication enabled func (svc user) SetPassword(userID uint64, newPassword string) (err error) { - log := svc.log(svc.ctx, zap.Uint64("userID", userID)) + var ( + u *types.User + uaProps = &userActionProps{user: &types.User{ID: userID}} + ) - if !svc.settings.Auth.Internal.Enabled { - return errors.New("internal authentication disabled") - } + err = svc.db.Transaction(func() error { + if u, err = svc.user.FindByID(userID); err != nil { + return err + } - var u *types.User - if u, err = svc.user.FindByID(userID); err != nil { - return - } + uaProps.setUser(u) - if !svc.ac.CanUpdateUser(svc.ctx, u) { - return ErrNoPermissions.withStack() - } + if !svc.ac.CanUpdateUser(svc.ctx, u) { + return UserErrNotAllowedToUpdate() + } - if err = svc.auth.checkPasswordStrength(newPassword); err != nil { - return - } + if !svc.auth.checkPasswordStrength(newPassword) { + return UserErrPasswordNotSecure() + } - return svc.db.Transaction(func() error { if err := svc.auth.changePassword(userID, newPassword); err != nil { return err } - log.Info("password changed") - return nil }) + + return svc.recordAction(svc.ctx, uaProps, UserActionSetPassword, err) + } // Masks (or leaves as-is) private data on user @@ -546,6 +639,56 @@ func (svc user) handlePrivateData(u *types.User) { } } +// Preloader collects all ids of users, loads them and sets them back +// +// +// @todo this kind of preloader is useful and can be implemented in bunch +// of places and replace old code +func (svc user) Preloader(g userIdGetter, f types.UserFilter, s userSetter) error { + var ( + // channel that will collect the IDs in the getter + ch = make(chan uint64, 0) + + // unique index for IDs + unq = make(map[uint64]bool) + ) + + // Reset the collection of the IDs + f.UserID = make([]uint64, 0) + + // Call getter and collect the IDs + go g(ch) + +rangeLoop: + for { + select { + case <-svc.ctx.Done(): + close(ch) + break rangeLoop + case id, ok := <-ch: + if !ok { + // Channel closed + break rangeLoop + } + + if !unq[id] { + unq[id] = true + f.UserID = append(f.UserID, id) + } + } + + } + + // Load all users (even if deleted, suspended) from the given list of IDs + uu, _, err := svc.Find(f) + + if err != nil { + return err + } + + return uu.Walk(s) +} + func createHandle(r repository.UserRepository, u *types.User) { if u.Handle == "" { u.Handle, _ = handle.Cast( diff --git a/system/service/user_actions.gen.go b/system/service/user_actions.gen.go new file mode 100644 index 000000000..e2bd83790 --- /dev/null +++ b/system/service/user_actions.gen.go @@ -0,0 +1,1139 @@ +package service + +// This file is auto-generated from system/service/user_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + userActionProps struct { + user *types.User + new *types.User + update *types.User + existing *types.User + filter *types.UserFilter + } + + userAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *userActionProps + } + + userError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *userActionProps + } +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setUser updates userActionProps's user +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *userActionProps) setUser(user *types.User) *userActionProps { + p.user = user + return p +} + +// setNew updates userActionProps's new +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *userActionProps) setNew(new *types.User) *userActionProps { + p.new = new + return p +} + +// setUpdate updates userActionProps's update +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *userActionProps) setUpdate(update *types.User) *userActionProps { + p.update = update + return p +} + +// setExisting updates userActionProps's existing +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *userActionProps) setExisting(existing *types.User) *userActionProps { + p.existing = existing + return p +} + +// setFilter updates userActionProps's filter +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *userActionProps) setFilter(filter *types.UserFilter) *userActionProps { + p.filter = filter + return p +} + +// serialize converts userActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p userActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + str = func(i interface{}) string { return fmt.Sprintf("%v", i) } + ) + + if p.user != nil { + m["user.handle"] = str(p.user.Handle) + m["user.email"] = str(p.user.Email) + m["user.name"] = str(p.user.Name) + m["user.username"] = str(p.user.Username) + m["user.ID"] = str(p.user.ID) + } + if p.new != nil { + m["new.handle"] = str(p.new.Handle) + m["new.email"] = str(p.new.Email) + m["new.name"] = str(p.new.Name) + m["new.username"] = str(p.new.Username) + m["new.ID"] = str(p.new.ID) + } + if p.update != nil { + m["update.handle"] = str(p.update.Handle) + m["update.email"] = str(p.update.Email) + m["update.name"] = str(p.update.Name) + m["update.username"] = str(p.update.Username) + m["update.ID"] = str(p.update.ID) + } + if p.existing != nil { + m["existing.handle"] = str(p.existing.Handle) + m["existing.email"] = str(p.existing.Email) + m["existing.name"] = str(p.existing.Name) + m["existing.username"] = str(p.existing.Username) + m["existing.ID"] = str(p.existing.ID) + } + if p.filter != nil { + m["filter.query"] = str(p.filter.Query) + m["filter.userID"] = str(p.filter.UserID) + m["filter.roleID"] = str(p.filter.RoleID) + m["filter.handle"] = str(p.filter.Handle) + m["filter.email"] = str(p.filter.Email) + m["filter.username"] = str(p.filter.Username) + m["filter.deleted"] = str(p.filter.Deleted) + m["filter.suspended"] = str(p.filter.Suspended) + m["filter.sort"] = str(p.filter.Sort) + } + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p userActionProps) tr(in string, err error) string { + var pairs = []string{"{err}"} + + if err != nil { + for { + // Unwrap errors + ue := errors.Unwrap(err) + if ue == nil { + break + } + + err = ue + } + + pairs = append(pairs, err.Error()) + } else { + pairs = append(pairs, "nil") + } + + if p.user != nil { + pairs = append(pairs, "{user}", fmt.Sprintf("%v", p.user.Handle)) + pairs = append(pairs, "{user.handle}", fmt.Sprintf("%v", p.user.Handle)) + pairs = append(pairs, "{user.email}", fmt.Sprintf("%v", p.user.Email)) + pairs = append(pairs, "{user.name}", fmt.Sprintf("%v", p.user.Name)) + pairs = append(pairs, "{user.username}", fmt.Sprintf("%v", p.user.Username)) + pairs = append(pairs, "{user.ID}", fmt.Sprintf("%v", p.user.ID)) + } + + if p.new != nil { + pairs = append(pairs, "{new}", fmt.Sprintf("%v", p.new.Handle)) + pairs = append(pairs, "{new.handle}", fmt.Sprintf("%v", p.new.Handle)) + pairs = append(pairs, "{new.email}", fmt.Sprintf("%v", p.new.Email)) + pairs = append(pairs, "{new.name}", fmt.Sprintf("%v", p.new.Name)) + pairs = append(pairs, "{new.username}", fmt.Sprintf("%v", p.new.Username)) + pairs = append(pairs, "{new.ID}", fmt.Sprintf("%v", p.new.ID)) + } + + if p.update != nil { + pairs = append(pairs, "{update}", fmt.Sprintf("%v", p.update.Handle)) + pairs = append(pairs, "{update.handle}", fmt.Sprintf("%v", p.update.Handle)) + pairs = append(pairs, "{update.email}", fmt.Sprintf("%v", p.update.Email)) + pairs = append(pairs, "{update.name}", fmt.Sprintf("%v", p.update.Name)) + pairs = append(pairs, "{update.username}", fmt.Sprintf("%v", p.update.Username)) + pairs = append(pairs, "{update.ID}", fmt.Sprintf("%v", p.update.ID)) + } + + if p.existing != nil { + pairs = append(pairs, "{existing}", fmt.Sprintf("%v", p.existing.Handle)) + pairs = append(pairs, "{existing.handle}", fmt.Sprintf("%v", p.existing.Handle)) + pairs = append(pairs, "{existing.email}", fmt.Sprintf("%v", p.existing.Email)) + pairs = append(pairs, "{existing.name}", fmt.Sprintf("%v", p.existing.Name)) + pairs = append(pairs, "{existing.username}", fmt.Sprintf("%v", p.existing.Username)) + pairs = append(pairs, "{existing.ID}", fmt.Sprintf("%v", p.existing.ID)) + } + + if p.filter != nil { + pairs = append(pairs, "{filter}", fmt.Sprintf("%v", p.filter.Query)) + pairs = append(pairs, "{filter.query}", fmt.Sprintf("%v", p.filter.Query)) + pairs = append(pairs, "{filter.userID}", fmt.Sprintf("%v", p.filter.UserID)) + pairs = append(pairs, "{filter.roleID}", fmt.Sprintf("%v", p.filter.RoleID)) + pairs = append(pairs, "{filter.handle}", fmt.Sprintf("%v", p.filter.Handle)) + pairs = append(pairs, "{filter.email}", fmt.Sprintf("%v", p.filter.Email)) + pairs = append(pairs, "{filter.username}", fmt.Sprintf("%v", p.filter.Username)) + pairs = append(pairs, "{filter.deleted}", fmt.Sprintf("%v", p.filter.Deleted)) + pairs = append(pairs, "{filter.suspended}", fmt.Sprintf("%v", p.filter.Suspended)) + pairs = append(pairs, "{filter.sort}", fmt.Sprintf("%v", p.filter.Sort)) + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *userAction) String() string { + var props = &userActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *userAction) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error methods + +// String returns loggable description as string +// +// It falls back to message if log is not set +// +// This function is auto-generated. +// +func (e *userError) String() string { + var props = &userActionProps{} + + if e.props != nil { + props = e.props + } + + if e.wrap != nil && !strings.Contains(e.log, "{err}") { + // Suffix error log with {err} to ensure + // we log the cause for this error + e.log += ": {err}" + } + + return props.tr(e.log, e.wrap) +} + +// Error satisfies +// +// This function is auto-generated. +// +func (e *userError) Error() string { + var props = &userActionProps{} + + if e.props != nil { + props = e.props + } + + return props.tr(e.message, e.wrap) +} + +// Is fn for error equality check +// +// This function is auto-generated. +// +func (e *userError) Is(Resource error) bool { + t, ok := Resource.(*userError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps userError around another error +// +// This function is auto-generated. +// +func (e *userError) Wrap(err error) *userError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *userError) Unwrap() error { + return e.wrap +} + +func (e *userError) LoggableAction() *actionlog.Action { + return &actionlog.Action{ + Timestamp: e.timestamp, + Resource: e.resource, + Action: e.action, + Severity: e.severity, + Description: e.String(), + Error: e.Error(), + Meta: e.props.serialize(), + } +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action constructors + +// UserActionSearch returns "system:user.search" error +// +// This function is auto-generated. +// +func UserActionSearch(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "search", + log: "searched for matching users", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// UserActionLookup returns "system:user.lookup" error +// +// This function is auto-generated. +// +func UserActionLookup(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "lookup", + log: "looked-up for a {user}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// UserActionCreate returns "system:user.create" error +// +// This function is auto-generated. +// +func UserActionCreate(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "create", + log: "created {user}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// UserActionUpdate returns "system:user.update" error +// +// This function is auto-generated. +// +func UserActionUpdate(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "update", + log: "updated {user}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// UserActionDelete returns "system:user.delete" error +// +// This function is auto-generated. +// +func UserActionDelete(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "delete", + log: "deleted {user}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// UserActionUndelete returns "system:user.undelete" error +// +// This function is auto-generated. +// +func UserActionUndelete(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "undelete", + log: "undeleted {user}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// UserActionSuspend returns "system:user.suspend" error +// +// This function is auto-generated. +// +func UserActionSuspend(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "suspend", + log: "suspended {user}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// UserActionUnsuspend returns "system:user.unsuspend" error +// +// This function is auto-generated. +// +func UserActionUnsuspend(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "unsuspend", + log: "unsuspended {user}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// UserActionSetPassword returns "system:user.setPassword" error +// +// This function is auto-generated. +// +func UserActionSetPassword(props ...*userActionProps) *userAction { + a := &userAction{ + timestamp: time.Now(), + resource: "system:user", + action: "setPassword", + log: "password changed for {user}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// UserErrGeneric returns "system:user.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func UserErrGeneric(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNonexistent returns "system:user.nonexistent" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func UserErrNonexistent(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "nonexistent", + action: "error", + message: "user does not exist", + log: "user does not exist", + severity: actionlog.Warning, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrInvalidID returns "system:user.invalidID" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func UserErrInvalidID(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "invalidID", + action: "error", + message: "invalid ID", + log: "invalid ID", + severity: actionlog.Warning, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrInvalidHandle returns "system:user.invalidHandle" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func UserErrInvalidHandle(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "invalidHandle", + action: "error", + message: "invalid handle", + log: "invalid handle", + severity: actionlog.Warning, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrInvalidEmail returns "system:user.invalidEmail" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func UserErrInvalidEmail(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "invalidEmail", + action: "error", + message: "invalid email", + log: "invalid email", + severity: actionlog.Warning, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNotAllowedToRead returns "system:user.notAllowedToRead" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrNotAllowedToRead(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "notAllowedToRead", + action: "error", + message: "not allowed to read user", + log: "failed to read {user.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNotAllowedToListUsers returns "system:user.notAllowedToListUsers" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrNotAllowedToListUsers(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "notAllowedToListUsers", + action: "error", + message: "not allowed to list users", + log: "failed to list user; insufficient permissions", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNotAllowedToCreate returns "system:user.notAllowedToCreate" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrNotAllowedToCreate(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "notAllowedToCreate", + action: "error", + message: "not allowed to create user", + log: "failed to create user; insufficient permissions", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNotAllowedToUpdate returns "system:user.notAllowedToUpdate" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrNotAllowedToUpdate(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "notAllowedToUpdate", + action: "error", + message: "not allowed to update user", + log: "failed to update {user.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNotAllowedToDelete returns "system:user.notAllowedToDelete" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrNotAllowedToDelete(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "notAllowedToDelete", + action: "error", + message: "not allowed to delete user", + log: "failed to delete {user.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNotAllowedToUndelete returns "system:user.notAllowedToUndelete" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrNotAllowedToUndelete(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "notAllowedToUndelete", + action: "error", + message: "not allowed to undelete user", + log: "failed to undelete {user.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNotAllowedToSuspend returns "system:user.notAllowedToSuspend" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrNotAllowedToSuspend(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "notAllowedToSuspend", + action: "error", + message: "not allowed to suspend user", + log: "failed to suspend {user.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrNotAllowedToUnsuspend returns "system:user.notAllowedToUnsuspend" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrNotAllowedToUnsuspend(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "notAllowedToUnsuspend", + action: "error", + message: "not allowed to unsuspend user", + log: "failed to unsuspend {user.handle}; insufficient permissions", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrHandleNotUnique returns "system:user.handleNotUnique" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func UserErrHandleNotUnique(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "handleNotUnique", + action: "error", + message: "handle not unique", + log: "used duplicate handle ({user.handle}) for user", + severity: actionlog.Warning, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrEmailNotUnique returns "system:user.emailNotUnique" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func UserErrEmailNotUnique(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "emailNotUnique", + action: "error", + message: "email not unique", + log: "used duplicate email ({user.email}) for user", + severity: actionlog.Warning, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrUsernameNotUnique returns "system:user.usernameNotUnique" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func UserErrUsernameNotUnique(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "usernameNotUnique", + action: "error", + message: "username not unique", + log: "used duplicate username ({user.username}) for user", + severity: actionlog.Warning, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// UserErrPasswordNotSecure returns "system:user.passwordNotSecure" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func UserErrPasswordNotSecure(props ...*userActionProps) *userError { + var e = &userError{ + timestamp: time.Now(), + resource: "system:user", + error: "passwordNotSecure", + action: "error", + message: "provided password is not secure; use longer password with more non-alphanumeric character", + log: "provided password is not secure; use longer password with more non-alphanumeric character", + severity: actionlog.Alert, + props: func() *userActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* + +// recordAction is a service helper function wraps function that can return error +// +// context is used to enrich audit log entry with current user info, request ID, IP address... +// props are collected action/error properties +// action (optional) fn will be used to construct userAction struct from given props (and error) +// err is any error that occurred while action was happening +// +// Action has success and fail (error) state: +// - when recorded without an error (4th param), action is recorded as successful. +// - when an additional error is given (4th param), action is used to wrap +// the additional error +// +// This function is auto-generated. +// +func (svc user) recordAction(ctx context.Context, props *userActionProps, action func(...*userActionProps) *userAction, err error) error { + var ( + ok bool + + // Return error + retError *userError + + // Recorder error + recError *userError + ) + + if err != nil { + if retError, ok = err.(*userError); !ok { + // got non-user error, wrap it with UserErrGeneric + retError = UserErrGeneric(props).Wrap(err) + + // copy action to returning and recording error + retError.action = action().action + + // we'll use UserErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + // copy action to returning and recording error + retError.action = action().action + // start with copy of return error for recording + // this will be updated with tha root cause as we try and + // unwrap the error + recError = retError + + // find the original recError for this error + // for the purpose of logging + var unwrappedError error = retError + for { + if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil { + // nothing wrapped + break + } + + // update recError ONLY of wrapped error is of type userError + if unwrappedSinkError, ok := unwrappedError.(*userError); ok { + recError = unwrappedSinkError + } + } + + if retError.props == nil { + // set props on returning error if empty + retError.props = props + } + + if recError.props == nil { + // set props on recording error if empty + recError.props = props + } + } + } + + if svc.actionlog != nil { + if retError != nil { + // failed action, log error + svc.actionlog.Record(ctx, recError) + } else if action != nil { + // successful + svc.actionlog.Record(ctx, action(props)) + } + } + + if err == nil { + // retError not an interface and that WILL (!!) cause issues + // with nil check (== nil) when it is not explicitly returned + return nil + } + + return retError +} diff --git a/system/service/user_actions.yaml b/system/service/user_actions.yaml new file mode 100644 index 000000000..39ccb3687 --- /dev/null +++ b/system/service/user_actions.yaml @@ -0,0 +1,129 @@ +# List of loggable service actions + +resource: system:user +service: user + +# Default sensitivity for actions +defaultActionSeverity: notice + +# default severity for errors +defaultErrorSeverity: alert + +import: + - github.com/cortezaproject/corteza-server/system/types + +props: + - name: user + type: "*types.User" + fields: [ handle, email, name, username, ID ] + - name: new + type: "*types.User" + fields: [ handle, email, name, username, ID ] + - name: update + type: "*types.User" + fields: [ handle, email, name, username, ID ] + - name: existing + type: "*types.User" + fields: [ handle, email, name, username, ID ] + - name: filter + type: "*types.UserFilter" + fields: [ query, userID, roleID, handle, email, username, deleted, suspended, sort ] + +actions: + - action: search + log: "searched for matching users" + severity: info + + - action: lookup + log: "looked-up for a {user}" + severity: info + + - action: create + log: "created {user}" + + - action: update + log: "updated {user}" + + - action: delete + log: "deleted {user}" + + - action: undelete + log: "undeleted {user}" + + - action: suspend + log: "suspended {user}" + + - action: unsuspend + log: "unsuspended {user}" + + - action: setPassword + log: "password changed for {user}" + +errors: + - error: nonexistent + message: "user does not exist" + severity: warning + + - error: invalidID + message: "invalid ID" + severity: warning + + - error: invalidHandle + message: "invalid handle" + severity: warning + + - error: invalidEmail + message: "invalid email" + severity: warning + + + - error: notAllowedToRead + message: "not allowed to read user" + log: "failed to read {user.handle}; insufficient permissions" + + - error: notAllowedToListUsers + message: "not allowed to list users" + log: "failed to list user; insufficient permissions" + + - error: notAllowedToCreate + message: "not allowed to create user" + log: "failed to create user; insufficient permissions" + + - error: notAllowedToUpdate + message: "not allowed to update user" + log: "failed to update {user.handle}; insufficient permissions" + + - error: notAllowedToDelete + message: "not allowed to delete user" + log: "failed to delete {user.handle}; insufficient permissions" + + - error: notAllowedToUndelete + message: "not allowed to undelete user" + log: "failed to undelete {user.handle}; insufficient permissions" + + - error: notAllowedToSuspend + message: "not allowed to suspend user" + log: "failed to suspend {user.handle}; insufficient permissions" + + - error: notAllowedToUnsuspend + message: "not allowed to unsuspend user" + log: "failed to unsuspend {user.handle}; insufficient permissions" + + - error: handleNotUnique + message: "handle not unique" + log: "used duplicate handle ({user.handle}) for user" + severity: warning + + - error: emailNotUnique + message: "email not unique" + log: "used duplicate email ({user.email}) for user" + severity: warning + + - error: usernameNotUnique + message: "username not unique" + log: "used duplicate username ({user.username}) for user" + severity: warning + + - error: passwordNotSecure + message: "provided password is not secure; use longer password with more non-alphanumeric character" + diff --git a/tests/system/role_test.go b/tests/system/role_test.go index 64d1041e0..bb73e54a8 100644 --- a/tests/system/role_test.go +++ b/tests/system/role_test.go @@ -68,7 +68,7 @@ func TestRoleList(t *testing.T) { End() } -func TestRoleList_filterForbiden(t *testing.T) { +func TestRoleList_filterForbidden(t *testing.T) { h := newHelper(t) // @todo this can be a problematic test because it leaves @@ -98,7 +98,7 @@ func TestRoleCreateForbidden(t *testing.T) { FormData("name", rs()). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoCreatePermissions")). + Assert(helpers.AssertError("not allowed to create role")). End() } @@ -113,7 +113,7 @@ func TestRoleCreateNotUnique(t *testing.T) { FormData("handle", role.Handle). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.RoleHandleNotUnique")). + Assert(helpers.AssertError("role handle not unique")). End() h.apiInit(). @@ -122,7 +122,7 @@ func TestRoleCreateNotUnique(t *testing.T) { FormData("handle", "handle_"+rs()). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.RoleNameNotUnique")). + Assert(helpers.AssertError("role name not unique")). End() } @@ -150,7 +150,7 @@ func TestRoleUpdateForbidden(t *testing.T) { FormData("email", h.randEmail()). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoUpdatePermissions")). + Assert(helpers.AssertError("not allowed to update role")). End() } @@ -186,7 +186,7 @@ func TestRoleDeleteForbidden(t *testing.T) { Delete(fmt.Sprintf("/roles/%d", u.ID)). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoPermissions")). + Assert(helpers.AssertError("not allowed to delete role")). End() } diff --git a/tests/system/user_test.go b/tests/system/user_test.go index 155ae0698..66f056084 100644 --- a/tests/system/user_test.go +++ b/tests/system/user_test.go @@ -245,7 +245,7 @@ func TestUserCreateForbidden(t *testing.T) { FormData("email", h.randEmail()). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoCreatePermissions")). + Assert(helpers.AssertError("not allowed to create user")). End() } @@ -278,7 +278,7 @@ func TestUserUpdateForbidden(t *testing.T) { FormData("email", h.randEmail()). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoUpdatePermissions")). + Assert(helpers.AssertError("not allowed to update user")). End() } @@ -311,7 +311,7 @@ func TestUserDeleteForbidden(t *testing.T) { Delete(fmt.Sprintf("/users/%d", u.ID)). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoPermissions")). + Assert(helpers.AssertError("not allowed to delete user")). End() }