diff --git a/codegen/v2/actionlog/actions.gen.go.tpl b/codegen/v2/actionlog/actions.gen.go.tpl index 71a3047cb..35b578db6 100644 --- a/codegen/v2/actionlog/actions.gen.go.tpl +++ b/codegen/v2/actionlog/actions.gen.go.tpl @@ -90,6 +90,9 @@ func (p {{ $.Service }}ActionProps) serialize() actionlog.Meta { str = func(i interface{}) string { return fmt.Sprintf("%v", i) } ) + // avoiding declared but not used + _ = str + {{ range $prop := $.Props }} {{- if $prop.Builtin }} m["{{ $prop.Name }}"] = str(p.{{ $prop.Name }}) diff --git a/system/rest/stats.go b/system/rest/stats.go index 906e78419..6e8dd1679 100644 --- a/system/rest/stats.go +++ b/system/rest/stats.go @@ -17,7 +17,7 @@ type ( } statsService interface { - Metrics(context.Context) (interface{}, error) + Metrics(context.Context) (*service.StatisticsMetricsPayload, error) } ) diff --git a/system/service/access_control_actions.gen.go b/system/service/access_control_actions.gen.go index 9f1a8baea..4886d3883 100644 --- a/system/service/access_control_actions.gen.go +++ b/system/service/access_control_actions.gen.go @@ -71,6 +71,9 @@ func (p accessControlActionProps) serialize() actionlog.Meta { str = func(i interface{}) string { return fmt.Sprintf("%v", i) } ) + // avoiding declared but not used + _ = str + if p.rule != nil { m["rule.operation"] = str(p.rule.Operation) m["rule.roleID"] = str(p.rule.RoleID) diff --git a/system/service/application_actions.gen.go b/system/service/application_actions.gen.go index 48f370303..318848de4 100644 --- a/system/service/application_actions.gen.go +++ b/system/service/application_actions.gen.go @@ -107,6 +107,9 @@ func (p applicationActionProps) serialize() actionlog.Meta { str = func(i interface{}) string { return fmt.Sprintf("%v", i) } ) + // avoiding declared but not used + _ = str + if p.application != nil { m["application.name"] = str(p.application.Name) m["application.ID"] = str(p.application.ID) diff --git a/system/service/auth.go b/system/service/auth.go index ad03a58cc..671f353c6 100644 --- a/system/service/auth.go +++ b/system/service/auth.go @@ -522,15 +522,7 @@ func (svc auth) InternalLogin(email string, password string) (u *types.User, err } if !u.Valid() { - if u.SuspendedAt != nil { - err = ErrUserSuspended - } else if u.DeletedAt != nil { - err = ErrUserDeleted - } else { - err = ErrUserInvalid - } - u = nil - return err + return AuthErrFailedForDisabledUser() } if !u.EmailConfirmed { diff --git a/system/service/auth_actions.gen.go b/system/service/auth_actions.gen.go index fe30726fa..e62c0102a 100644 --- a/system/service/auth_actions.gen.go +++ b/system/service/auth_actions.gen.go @@ -119,6 +119,9 @@ func (p authActionProps) serialize() actionlog.Meta { str = func(i interface{}) string { return fmt.Sprintf("%v", i) } ) + // avoiding declared but not used + _ = str + m["email"] = str(p.email) m["provider"] = str(p.provider) if p.credentials != nil { diff --git a/system/service/auth_notification.go b/system/service/auth_notification.go index 6a468dfcb..de56dc170 100644 --- a/system/service/auth_notification.go +++ b/system/service/auth_notification.go @@ -3,6 +3,7 @@ package service import ( "bytes" "context" + "fmt" "html/template" "go.uber.org/zap" @@ -16,10 +17,8 @@ import ( type ( authNotification struct { - ctx context.Context - logger *zap.Logger - - // @todo merge auth & system settings + ctx context.Context + logger *zap.Logger settings *types.Settings } @@ -105,7 +104,7 @@ func (svc authNotification) send(name, lang string, payload authNotificationPayl ntf.SetBody("text/html", svc.render(svc.settings.Auth.Mail.PasswordReset.Body, payload)) default: - return ErrNoEmailTemplateForGivenOperation + return fmt.Errorf("unknown notification email template %q", name) } svc.log(svc.ctx).Debug( diff --git a/system/service/error.go b/system/service/error.go index aec50486c..d7fbd5fba 100644 --- a/system/service/error.go +++ b/system/service/error.go @@ -9,23 +9,8 @@ type ( ) const ( - ErrInvalidID serviceError = "InvalidID" - ErrInvalidHandle serviceError = "InvalidHandle" - ErrNoPermissions serviceError = "NoPermissions" - ErrNoGrantPermissions serviceError = "NoGrantPermissions" - ErrNoCreatePermissions serviceError = "NoCreatePermissions" - ErrNoUpdatePermissions serviceError = "NoUpdatePermissions" - ErrNoDeletePermissions serviceError = "NoDeletePermissions" - ErrNoReadPermissions serviceError = "NoReadPermissions" - ErrNoTriggerManagementPermissions serviceError = "NoTriggerManagementPermissions" - ErrNoScriptCreatePermissions serviceError = "NoScriptCreatePermissions" - ErrNoReminderAssignPermissions serviceError = "NoReminderAssignPermissions" - - ErrUserSuspended serviceError = "UserSuspended" - ErrUserDeleted serviceError = "UserDeleted" - ErrUserInvalid serviceError = "UserInvalid" - - ErrNoEmailTemplateForGivenOperation serviceError = "NoEmailTemplateForGivenOperation" + ErrNoUpdatePermissions serviceError = "NoUpdatePermissions" + ErrNoReminderAssignPermissions serviceError = "NoReminderAssignPermissions" ) func (e serviceError) Error() string { diff --git a/system/service/role_actions.gen.go b/system/service/role_actions.gen.go index b32469ee3..338908d08 100644 --- a/system/service/role_actions.gen.go +++ b/system/service/role_actions.gen.go @@ -143,6 +143,9 @@ func (p roleActionProps) serialize() actionlog.Meta { str = func(i interface{}) string { return fmt.Sprintf("%v", i) } ) + // avoiding declared but not used + _ = str + if p.member != nil { m["member.handle"] = str(p.member.Handle) m["member.email"] = str(p.member.Email) diff --git a/system/service/service.go b/system/service/service.go index 65214087f..104afda5d 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -158,7 +158,7 @@ func Initialize(ctx context.Context, log *zap.Logger, c Config) (err error) { DefaultApplication = Application(ctx) DefaultReminder = Reminder(ctx) DefaultSink = Sink() - DefaultStatistics = Statistics(ctx) + DefaultStatistics = Statistics() DefaultAttachment = Attachment(DefaultStore) return diff --git a/system/service/sink_actions.gen.go b/system/service/sink_actions.gen.go index ab0a739da..746de6ddb 100644 --- a/system/service/sink_actions.gen.go +++ b/system/service/sink_actions.gen.go @@ -122,6 +122,9 @@ func (p sinkActionProps) serialize() actionlog.Meta { str = func(i interface{}) string { return fmt.Sprintf("%v", i) } ) + // avoiding declared but not used + _ = str + m["url"] = str(p.url) m["responseStatus"] = str(p.responseStatus) m["contentType"] = str(p.contentType) diff --git a/system/service/statistics.go b/system/service/statistics.go index 1def794e3..f0ebd0043 100644 --- a/system/service/statistics.go +++ b/system/service/statistics.go @@ -3,71 +3,59 @@ package service import ( "context" - "github.com/titpetric/factory" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - - "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/system/repository" "github.com/cortezaproject/corteza-server/system/types" ) type ( statistics struct { - db *factory.DB - ctx context.Context - logger *zap.Logger - - ac statisticsAccessController + actionlog actionlog.Recorder + ac statisticsAccessController } statisticsAccessController interface { CanAccess(context.Context) bool } + + StatisticsMetricsPayload struct { + Users *types.UserMetrics `json:"users"` + Roles *types.RoleMetrics `json:"roles"` + Applications *types.ApplicationMetrics `json:"applications"` + } ) -func Statistics(ctx context.Context) *statistics { +func Statistics() *statistics { return &statistics{ - db: repository.DB(ctx), - ac: DefaultAccessControl, - logger: DefaultLogger.Named("statistics"), + actionlog: DefaultActionlog, + ac: DefaultAccessControl, } } -// log() returns zap's logger with requestID from current context and fields. -func (svc statistics) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(ctx, svc.logger).With(fields...) -} +func (svc statistics) Metrics(ctx context.Context) (rval *StatisticsMetricsPayload, err error) { + db := repository.DB(ctx) -func (svc statistics) Metrics(ctx context.Context) (interface{}, error) { - if !svc.ac.CanAccess(ctx) { - return nil, ErrNoPermissions - } - - type ( - metricsPayload struct { - Users *types.UserMetrics `json:"users"` - Roles *types.RoleMetrics `json:"roles"` - Applications *types.ApplicationMetrics `json:"applications"` + err = db.Transaction(func() error { + if !svc.ac.CanAccess(ctx) { + return StatisticsErrNotAllowedToReadStatistics() } - ) - var ( - rval = &metricsPayload{} - err error - ) + rval = &StatisticsMetricsPayload{} - if rval.Users, err = repository.User(ctx, svc.db).Metrics(); err != nil { - return nil, err - } + if rval.Users, err = repository.User(ctx, db).Metrics(); err != nil { + return err + } - if rval.Roles, err = repository.Role(ctx, svc.db).Metrics(); err != nil { - return nil, err - } + if rval.Roles, err = repository.Role(ctx, db).Metrics(); err != nil { + return err + } - if rval.Applications, err = repository.Application(ctx, svc.db).Metrics(); err != nil { - return nil, err - } + if rval.Applications, err = repository.Application(ctx, db).Metrics(); err != nil { + return err + } - return rval, err + return nil + }) + + return rval, svc.recordAction(ctx, &statisticsActionProps{}, StatisticsActionServe, err) } diff --git a/system/service/statistics_actions.gen.go b/system/service/statistics_actions.gen.go new file mode 100644 index 000000000..9030cf3ba --- /dev/null +++ b/system/service/statistics_actions.gen.go @@ -0,0 +1,383 @@ +package service + +// This file is auto-generated from system/service/statistics_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/pkg/actionlog" +) + +type ( + statisticsActionProps struct { + } + + statisticsAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *statisticsActionProps + } + + statisticsError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *statisticsActionProps + } +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods + +// serialize converts statisticsActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p statisticsActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + str = func(i interface{}) string { return fmt.Sprintf("%v", i) } + ) + + // avoiding declared but not used + _ = str + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p statisticsActionProps) 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") + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *statisticsAction) String() string { + var props = &statisticsActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *statisticsAction) 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 *statisticsError) String() string { + var props = &statisticsActionProps{} + + 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 *statisticsError) Error() string { + var props = &statisticsActionProps{} + + 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 *statisticsError) Is(Resource error) bool { + t, ok := Resource.(*statisticsError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps statisticsError around another error +// +// This function is auto-generated. +// +func (e *statisticsError) Wrap(err error) *statisticsError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *statisticsError) Unwrap() error { + return e.wrap +} + +func (e *statisticsError) 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 + +// StatisticsActionServe returns "system:statistics.serve" error +// +// This function is auto-generated. +// +func StatisticsActionServe(props ...*statisticsActionProps) *statisticsAction { + a := &statisticsAction{ + timestamp: time.Now(), + resource: "system:statistics", + action: "serve", + log: "metrics served", + severity: actionlog.Debug, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// StatisticsErrGeneric returns "system:statistics.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func StatisticsErrGeneric(props ...*statisticsActionProps) *statisticsError { + var e = &statisticsError{ + timestamp: time.Now(), + resource: "system:statistics", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *statisticsActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// StatisticsErrNotAllowedToReadStatistics returns "system:statistics.notAllowedToReadStatistics" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func StatisticsErrNotAllowedToReadStatistics(props ...*statisticsActionProps) *statisticsError { + var e = &statisticsError{ + timestamp: time.Now(), + resource: "system:statistics", + error: "notAllowedToReadStatistics", + action: "error", + message: "not allowed to read statistics", + log: "not allowed to read statistics", + severity: actionlog.Warning, + props: func() *statisticsActionProps { + 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 statisticsAction 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 statistics) recordAction(ctx context.Context, props *statisticsActionProps, action func(...*statisticsActionProps) *statisticsAction, err error) error { + var ( + ok bool + + // Return error + retError *statisticsError + + // Recorder error + recError *statisticsError + ) + + if err != nil { + if retError, ok = err.(*statisticsError); !ok { + // got non-statistics error, wrap it with StatisticsErrGeneric + retError = StatisticsErrGeneric(props).Wrap(err) + + // copy action to returning and recording error + retError.action = action().action + + // we'll use StatisticsErrGeneric 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 statisticsError + if unwrappedSinkError, ok := unwrappedError.(*statisticsError); 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/statistics_actions.yaml b/system/service/statistics_actions.yaml new file mode 100644 index 000000000..565a7b6c7 --- /dev/null +++ b/system/service/statistics_actions.yaml @@ -0,0 +1,19 @@ +# List of security/audit events and errors that we need to log + +resource: system:statistics +service: statistics + +# Default sensitivity for actions +defaultActionSeverity: debug + +# default severity for errors +defaultErrorSeverity: error + +actions: + - action: serve + log: "metrics served" + +errors: + - error: notAllowedToReadStatistics + message: "not allowed to read statistics" + severity: warning diff --git a/system/service/user.go b/system/service/user.go index dd2954a9b..9a758d141 100644 --- a/system/service/user.go +++ b/system/service/user.go @@ -348,7 +348,7 @@ func (svc user) Update(upd *types.User) (u *types.User, err error) { err = svc.db.Transaction(func() (err error) { if upd.ID == 0 { - return ErrInvalidID + return UserErrInvalidID() } if !handle.IsValid(upd.Handle) { diff --git a/system/service/user_actions.gen.go b/system/service/user_actions.gen.go index e2bd83790..47ecdac31 100644 --- a/system/service/user_actions.gen.go +++ b/system/service/user_actions.gen.go @@ -119,6 +119,9 @@ func (p userActionProps) serialize() actionlog.Meta { str = func(i interface{}) string { return fmt.Sprintf("%v", i) } ) + // avoiding declared but not used + _ = str + if p.user != nil { m["user.handle"] = str(p.user.Handle) m["user.email"] = str(p.user.Email)