diff --git a/compose/service/attachment.go b/compose/service/attachment.go index f216ab368..ecafe94b3 100644 --- a/compose/service/attachment.go +++ b/compose/service/attachment.go @@ -14,14 +14,11 @@ import ( "github.com/edwvee/exiffix" "github.com/pkg/errors" "github.com/titpetric/factory" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" "github.com/cortezaproject/corteza-server/compose/repository" "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/auth" - "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/store" ) @@ -32,9 +29,8 @@ const ( type ( attachment struct { - db *factory.DB - ctx context.Context - logger *zap.Logger + db *factory.DB + ctx context.Context actionlog actionlog.Recorder @@ -99,11 +95,6 @@ func (svc attachment) With(ctx context.Context) AttachmentService { } } -// log() returns zap's logger with requestID from current context and fields. -func (svc attachment) log(fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(svc.ctx, svc.logger).With(fields...) -} - func (svc attachment) FindByID(namespaceID, attachmentID uint64) (att *types.Attachment, err error) { var ( aProps = &attachmentActionProps{} diff --git a/messaging/repository/error.go b/messaging/repository/error.go index 3684427f2..2c8716406 100644 --- a/messaging/repository/error.go +++ b/messaging/repository/error.go @@ -23,6 +23,10 @@ func (e repositoryError) String() string { return "messaging.repository." + string(e) } +func (e repositoryError) Eq(err error) bool { + return err != nil && e.Error() == err.Error() +} + func (e repositoryError) New() error { return errors.WithStack(e) } diff --git a/messaging/rest/attachment.go b/messaging/rest/attachment.go index 404b888f2..11640f49f 100644 --- a/messaging/rest/attachment.go +++ b/messaging/rest/attachment.go @@ -2,20 +2,18 @@ package rest import ( "context" + "errors" + "fmt" "io" "net/http" "net/url" - "github.com/pkg/errors" - "github.com/cortezaproject/corteza-server/messaging/repository" "github.com/cortezaproject/corteza-server/messaging/rest/request" "github.com/cortezaproject/corteza-server/messaging/service" "github.com/cortezaproject/corteza-server/pkg/auth" ) -var _ = errors.Wrap - type ( Attachment struct { att service.AttachmentService @@ -46,19 +44,19 @@ func (ctrl *Attachment) Preview(ctx context.Context, r *request.AttachmentPrevie func (ctrl Attachment) isAccessible(attachmentID, userID uint64, signature string) error { if signature == "" { - return errors.New("Unauthorized") + return fmt.Errorf("Unauthorized") } if userID == 0 { - return errors.New("missing or invalid user ID") + return fmt.Errorf("missing or invalid user ID") } if attachmentID == 0 { - return errors.New("missing or invalid attachment ID") + return fmt.Errorf("missing or invalid attachment ID") } if !auth.DefaultSigner.Verify(signature, userID, attachmentID) { - return errors.New("missing or invalid signature") + return fmt.Errorf("missing or invalid signature") } return nil @@ -67,12 +65,10 @@ func (ctrl Attachment) isAccessible(attachmentID, userID uint64, signature strin func (ctrl Attachment) serve(ctx context.Context, ID uint64, preview, download bool) (interface{}, error) { return func(w http.ResponseWriter, req *http.Request) { att, err := ctrl.att.With(ctx).FindByID(ID) - if err != nil { - switch { - case err == repository.ErrAttachmentNotFound: + if errors.Is(err, repository.ErrAttachmentNotFound) { w.WriteHeader(http.StatusNotFound) - default: + } else { http.Error(w, err.Error(), http.StatusInternalServerError) } diff --git a/messaging/rest/channel.go b/messaging/rest/channel.go index d9b5bd66a..722385112 100644 --- a/messaging/rest/channel.go +++ b/messaging/rest/channel.go @@ -125,7 +125,7 @@ func (ctrl *Channel) Attach(ctx context.Context, r *request.ChannelAttach) (inte defer file.Close() - att, err := ctrl.svc.att.With(ctx).Create( + att, err := ctrl.svc.att.With(ctx).CreateMessageAttachment( r.Upload.Filename, r.Upload.Size, file, diff --git a/messaging/service/access_control.go b/messaging/service/access_control.go index 190fb948b..356d7e15e 100644 --- a/messaging/service/access_control.go +++ b/messaging/service/access_control.go @@ -4,6 +4,7 @@ import ( "context" "github.com/cortezaproject/corteza-server/messaging/types" + "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/permissions" ) @@ -11,6 +12,7 @@ import ( type ( accessControl struct { permissions accessControlPermissionServicer + actionlog actionlog.Recorder } accessControlPermissionServicer interface { @@ -28,6 +30,7 @@ type ( func AccessControl(perm accessControlPermissionServicer) *accessControl { return &accessControl{ permissions: perm, + actionlog: DefaultActionlog, } } @@ -227,15 +230,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/messaging/service/access_control_actions.gen.go b/messaging/service/access_control_actions.gen.go new file mode 100644 index 000000000..9d8a9e6e3 --- /dev/null +++ b/messaging/service/access_control_actions.gen.go @@ -0,0 +1,437 @@ +package service + +// This file is auto-generated from messaging/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 + } +) + +var ( + // just a placeholder to cover template cases w/o fmt package use + _ = fmt.Println +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// 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) + ) + + if p.rule != nil { + m.Set("rule.operation", p.rule.Operation, true) + m.Set("rule.roleID", p.rule.RoleID, true) + m.Set("rule.access", p.rule.Access, true) + m.Set("rule.resource", p.rule.Resource, true) + } + + 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}"} + // first non-empty string + fns = func(ii ...interface{}) string { + for _, i := range ii { + if s := fmt.Sprintf("%v", i); len(s) > 0 { + return s + } + } + + return "" + } + ) + + 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 { + // replacement for "{rule}" (in order how fields are defined) + pairs = append( + pairs, + "{rule}", + fns( + p.rule.Operation, + p.rule.RoleID, + p.rule.Access, + p.rule.Resource, + ), + ) + pairs = append(pairs, "{rule.operation}", fns(p.rule.Operation)) + pairs = append(pairs, "{rule.roleID}", fns(p.rule.RoleID)) + pairs = append(pairs, "{rule.access}", fns(p.rule.Access)) + pairs = append(pairs, "{rule.resource}", fns(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 "messaging:access_control.grant" error +// +// This function is auto-generated. +// +func AccessControlActionGrant(props ...*accessControlActionProps) *accessControlAction { + a := &accessControlAction{ + timestamp: time.Now(), + resource: "messaging:access_control", + action: "grant", + log: "grant", + severity: actionlog.Error, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// AccessControlErrGeneric returns "messaging: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: "messaging: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 "messaging: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: "messaging: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) + + if action != nil { + // 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 { + if action != 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/messaging/service/access_control_actions.yaml b/messaging/service/access_control_actions.yaml new file mode 100644 index 000000000..75cee5eb0 --- /dev/null +++ b/messaging/service/access_control_actions.yaml @@ -0,0 +1,25 @@ +# List of security/audit events and errors that we need to log + +resource: messaging: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/messaging/service/attachment.go b/messaging/service/attachment.go index 0296b37cc..3ffd386e2 100644 --- a/messaging/service/attachment.go +++ b/messaging/service/attachment.go @@ -3,6 +3,7 @@ package service import ( "bytes" "context" + "fmt" "image" "image/gif" "io" @@ -12,15 +13,13 @@ import ( "github.com/disintegration/imaging" "github.com/edwvee/exiffix" - "github.com/pkg/errors" + "github.com/titpetric/factory" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" "github.com/cortezaproject/corteza-server/messaging/repository" "github.com/cortezaproject/corteza-server/messaging/types" - "github.com/cortezaproject/corteza-server/pkg/auth" - "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/cortezaproject/corteza-server/pkg/actionlog" + intAuth "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/store" ) @@ -31,18 +30,19 @@ const ( type ( attachment struct { - db *factory.DB - ctx context.Context - logger *zap.Logger + db *factory.DB + ctx context.Context + + actionlog actionlog.Recorder ac attachmentAccessController - store store.Store - event EventService - channel ChannelService + store store.Store + event EventService attachment repository.AttachmentRepository message repository.MessageRepository + channel repository.ChannelRepository } attachmentAccessController interface { @@ -53,7 +53,7 @@ type ( With(ctx context.Context) AttachmentService FindByID(id uint64) (*types.Attachment, error) - Create(name string, size int64, fh io.ReadSeeker, channelId, replyTo uint64) (*types.Attachment, error) + CreateMessageAttachment(name string, size int64, fh io.ReadSeeker, channelId, replyTo uint64) (*types.Attachment, error) OpenOriginal(att *types.Attachment) (io.ReadSeeker, error) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) } @@ -61,35 +61,29 @@ type ( func Attachment(ctx context.Context, store store.Store) AttachmentService { return (&attachment{ - logger: DefaultLogger.Named("attachment"), - ac: DefaultAccessControl, - channel: DefaultChannel, - store: store, + ac: DefaultAccessControl, + store: store, }).With(ctx) } func (svc attachment) With(ctx context.Context) AttachmentService { db := repository.DB(ctx) return &attachment{ - ctx: ctx, - db: db, - ac: svc.ac, - logger: svc.logger, + ctx: ctx, + db: db, + ac: svc.ac, - store: svc.store, - event: Event(ctx), - channel: svc.channel.With(ctx), + actionlog: DefaultActionlog, + + store: svc.store, + event: Event(ctx), attachment: repository.Attachment(ctx, db), message: repository.Message(ctx, db), + channel: repository.Channel(ctx, db), } } -// log() returns zap's logger with requestID from current context and fields. -func (svc attachment) log(fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(svc.ctx, svc.logger).With(fields...) -} - func (svc attachment) FindByID(id uint64) (*types.Attachment, error) { return svc.attachment.FindAttachmentByID(id) } @@ -110,61 +104,47 @@ func (svc attachment) OpenPreview(att *types.Attachment) (io.ReadSeeker, error) return svc.store.Open(att.PreviewUrl) } -func (svc attachment) Create(name string, size int64, fh io.ReadSeeker, channelId, replyTo uint64) (att *types.Attachment, err error) { - if svc.store == nil { - return nil, errors.New("Can not create attachment: store handler not set") - } +func (svc attachment) CreateMessageAttachment(name string, size int64, fh io.ReadSeeker, channelID, replyTo uint64) (att *types.Attachment, err error) { + var ( + aProps = &attachmentActionProps{channel: &types.Channel{ID: channelID}, replyTo: replyTo} - var currentUserID uint64 = auth.GetIdentityFromContext(svc.ctx).Identity() - - if ch, err := svc.channel.FindByID(channelId); err != nil { - return nil, err - } else if !svc.ac.CanAttachMessage(svc.ctx, ch) { - return nil, ErrNoPermissions.withStack() - } - - att = &types.Attachment{ - ID: factory.Sonyflake.NextID(), - UserID: currentUserID, - Name: strings.TrimSpace(name), - } - - log := svc.log( - zap.String("name", att.Name), - zap.Int64("size", att.Meta.Original.Size), + currentUserID = intAuth.GetIdentityFromContext(svc.ctx).Identity() + ch *types.Channel ) - // Extract extension but make sure path.Ext is not confused by any leading/trailing dots - att.Meta.Original.Extension = strings.Trim(path.Ext(strings.Trim(name, ".")), ".") + err = svc.db.Transaction(func() (err error) { + if ch, err = svc.channel.FindByID(channelID); err != nil { + if repository.ErrChannelNotFound.Eq(err) { + return AttachmentErrChannelNotFound() + } + } - att.Meta.Original.Size = size - if att.Meta.Original.Mimetype, err = svc.extractMimetype(fh); err != nil { - log.Error("could not extract mime-type", zap.Error(err)) - return - } + aProps.setChannel(ch) - att.Url = svc.store.Original(att.ID, att.Meta.Original.Extension) - if err = svc.store.Save(att.Url, fh); err != nil { - log.Error("could not store file", zap.Error(err)) - return - } + if !svc.ac.CanAttachMessage(svc.ctx, ch) { + return AttachmentErrNotAllowedToAttachToChannel() + } - // Process image: extract width, height, make preview - err = svc.processImage(fh, att) - if err != nil { - log.Error("could not process image", zap.Error(err)) - } + att = &types.Attachment{ + ID: factory.Sonyflake.NextID(), + UserID: currentUserID, + Name: strings.TrimSpace(name), + } + + err = svc.create(name, size, fh, att) + if err != nil { + return err + } - return att, svc.db.Transaction(func() (err error) { if att, err = svc.attachment.CreateAttachment(att); err != nil { - return + return err } msg := &types.Message{ Attachment: att, Message: name, Type: types.MessageTypeAttachment, - ChannelID: channelId, + ChannelID: channelID, ReplyTo: replyTo, UserID: currentUserID, } @@ -179,12 +159,52 @@ func (svc attachment) Create(name string, size int64, fh io.ReadSeeker, channelI return } + aProps.setMessageID(msg.ID) + if err = svc.attachment.BindAttachment(att.ID, msg.ID); err != nil { return } return svc.sendEvent(msg) }) + + return att, svc.recordAction(svc.ctx, aProps, AttachmentActionCreate, err) +} + +func (svc attachment) create(name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) { + var ( + aProps = &attachmentActionProps{} + ) + + if svc.store == nil { + return fmt.Errorf("can not create attachment: store handler not set") + } + + aProps.setName(name) + aProps.setSize(size) + + // Extract extension but make sure path.Ext is not confused by any leading/trailing dots + att.Meta.Original.Extension = strings.Trim(path.Ext(strings.Trim(name, ".")), ".") + + att.Meta.Original.Size = size + if att.Meta.Original.Mimetype, err = svc.extractMimetype(fh); err != nil { + return AttachmentErrFailedToExtractMimeType(aProps).Wrap(err) + } + + att.Url = svc.store.Original(att.ID, att.Meta.Original.Extension) + aProps.setUrl(att.Url) + + if err = svc.store.Save(att.Url, fh); err != nil { + return AttachmentErrFailedToStoreFile(aProps).Wrap(err) + } + + // Process image: extract width, height, make preview + err = svc.processImage(fh, att) + if err != nil { + return AttachmentErrFailedToProcessImage(aProps).Wrap(err) + } + + return nil } func (svc attachment) extractMimetype(file io.ReadSeeker) (mimetype string, err error) { @@ -232,7 +252,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment } if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil { - return errors.Wrapf(err, "Could not get format from extension '%s'", att.Meta.Original.Extension) + return fmt.Errorf("Could not get format from extension '%s': %w", att.Meta.Original.Extension, err) } previewFormat = format @@ -253,7 +273,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment // Use first image for the preview preview = cfg.Image[0] } else { - return errors.Wrapf(err, "Could not decode gif config") + return fmt.Errorf("could not decode gif config: %w", err) } } else { @@ -268,7 +288,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment // other cases are handled here if preview == nil { if preview, err = imaging.Decode(original); err != nil { - return errors.Wrapf(err, "Could not decode original image") + return fmt.Errorf("could not decode original image: %w", err) } } diff --git a/messaging/service/attachment_actions.gen.go b/messaging/service/attachment_actions.gen.go new file mode 100644 index 000000000..bb74f50e5 --- /dev/null +++ b/messaging/service/attachment_actions.gen.go @@ -0,0 +1,863 @@ +package service + +// This file is auto-generated from messaging/service/attachment_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/messaging/types" + "github.com/cortezaproject/corteza-server/pkg/actionlog" +) + +type ( + attachmentActionProps struct { + messageID uint64 + replyTo uint64 + size int64 + name string + mimetype string + url string + attachment *types.Attachment + channel *types.Channel + } + + attachmentAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *attachmentActionProps + } + + attachmentError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *attachmentActionProps + } +) + +var ( + // just a placeholder to cover template cases w/o fmt package use + _ = fmt.Println +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setMessageID updates attachmentActionProps's messageID +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *attachmentActionProps) setMessageID(messageID uint64) *attachmentActionProps { + p.messageID = messageID + return p +} + +// setReplyTo updates attachmentActionProps's replyTo +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *attachmentActionProps) setReplyTo(replyTo uint64) *attachmentActionProps { + p.replyTo = replyTo + return p +} + +// setSize updates attachmentActionProps's size +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *attachmentActionProps) setSize(size int64) *attachmentActionProps { + p.size = size + return p +} + +// setName updates attachmentActionProps's name +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *attachmentActionProps) setName(name string) *attachmentActionProps { + p.name = name + return p +} + +// setMimetype updates attachmentActionProps's mimetype +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *attachmentActionProps) setMimetype(mimetype string) *attachmentActionProps { + p.mimetype = mimetype + return p +} + +// setUrl updates attachmentActionProps's url +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *attachmentActionProps) setUrl(url string) *attachmentActionProps { + p.url = url + return p +} + +// setAttachment updates attachmentActionProps's attachment +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *attachmentActionProps) setAttachment(attachment *types.Attachment) *attachmentActionProps { + p.attachment = attachment + return p +} + +// setChannel updates attachmentActionProps's channel +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *attachmentActionProps) setChannel(channel *types.Channel) *attachmentActionProps { + p.channel = channel + return p +} + +// serialize converts attachmentActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p attachmentActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + ) + + m.Set("messageID", p.messageID, true) + m.Set("replyTo", p.replyTo, true) + m.Set("size", p.size, true) + m.Set("name", p.name, true) + m.Set("mimetype", p.mimetype, true) + m.Set("url", p.url, true) + if p.attachment != nil { + m.Set("attachment.name", p.attachment.Name, true) + m.Set("attachment.url", p.attachment.Url, true) + m.Set("attachment.previewUrl", p.attachment.PreviewUrl, true) + m.Set("attachment.meta", p.attachment.Meta, true) + m.Set("attachment.userID", p.attachment.UserID, true) + m.Set("attachment.ID", p.attachment.ID, true) + } + if p.channel != nil { + m.Set("channel.name", p.channel.Name, true) + m.Set("channel.topic", p.channel.Topic, true) + m.Set("channel.type", p.channel.Type, true) + m.Set("channel.ID", p.channel.ID, true) + } + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p attachmentActionProps) tr(in string, err error) string { + var ( + pairs = []string{"{err}"} + // first non-empty string + fns = func(ii ...interface{}) string { + for _, i := range ii { + if s := fmt.Sprintf("%v", i); len(s) > 0 { + return s + } + } + + return "" + } + ) + + 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, "{messageID}", fns(p.messageID)) + pairs = append(pairs, "{replyTo}", fns(p.replyTo)) + pairs = append(pairs, "{size}", fns(p.size)) + pairs = append(pairs, "{name}", fns(p.name)) + pairs = append(pairs, "{mimetype}", fns(p.mimetype)) + pairs = append(pairs, "{url}", fns(p.url)) + + if p.attachment != nil { + // replacement for "{attachment}" (in order how fields are defined) + pairs = append( + pairs, + "{attachment}", + fns( + p.attachment.Name, + p.attachment.Url, + p.attachment.PreviewUrl, + p.attachment.Meta, + p.attachment.UserID, + p.attachment.ID, + ), + ) + pairs = append(pairs, "{attachment.name}", fns(p.attachment.Name)) + pairs = append(pairs, "{attachment.url}", fns(p.attachment.Url)) + pairs = append(pairs, "{attachment.previewUrl}", fns(p.attachment.PreviewUrl)) + pairs = append(pairs, "{attachment.meta}", fns(p.attachment.Meta)) + pairs = append(pairs, "{attachment.userID}", fns(p.attachment.UserID)) + pairs = append(pairs, "{attachment.ID}", fns(p.attachment.ID)) + } + + if p.channel != nil { + // replacement for "{channel}" (in order how fields are defined) + pairs = append( + pairs, + "{channel}", + fns( + p.channel.Name, + p.channel.Topic, + p.channel.Type, + p.channel.ID, + ), + ) + pairs = append(pairs, "{channel.name}", fns(p.channel.Name)) + pairs = append(pairs, "{channel.topic}", fns(p.channel.Topic)) + pairs = append(pairs, "{channel.type}", fns(p.channel.Type)) + pairs = append(pairs, "{channel.ID}", fns(p.channel.ID)) + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *attachmentAction) String() string { + var props = &attachmentActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *attachmentAction) 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 *attachmentError) String() string { + var props = &attachmentActionProps{} + + 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 *attachmentError) Error() string { + var props = &attachmentActionProps{} + + 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 *attachmentError) Is(Resource error) bool { + t, ok := Resource.(*attachmentError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps attachmentError around another error +// +// This function is auto-generated. +// +func (e *attachmentError) Wrap(err error) *attachmentError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *attachmentError) Unwrap() error { + return e.wrap +} + +func (e *attachmentError) 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 + +// AttachmentActionSearch returns "messaging:attachment.search" error +// +// This function is auto-generated. +// +func AttachmentActionSearch(props ...*attachmentActionProps) *attachmentAction { + a := &attachmentAction{ + timestamp: time.Now(), + resource: "messaging:attachment", + action: "search", + log: "searched for attachments", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AttachmentActionLookup returns "messaging:attachment.lookup" error +// +// This function is auto-generated. +// +func AttachmentActionLookup(props ...*attachmentActionProps) *attachmentAction { + a := &attachmentAction{ + timestamp: time.Now(), + resource: "messaging:attachment", + action: "lookup", + log: "looked-up for a {attachment}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AttachmentActionCreate returns "messaging:attachment.create" error +// +// This function is auto-generated. +// +func AttachmentActionCreate(props ...*attachmentActionProps) *attachmentAction { + a := &attachmentAction{ + timestamp: time.Now(), + resource: "messaging:attachment", + action: "create", + log: "created {attachment} on {channel}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// AttachmentActionDelete returns "messaging:attachment.delete" error +// +// This function is auto-generated. +// +func AttachmentActionDelete(props ...*attachmentActionProps) *attachmentAction { + a := &attachmentAction{ + timestamp: time.Now(), + resource: "messaging:attachment", + action: "delete", + log: "deleted {attachment} from {channel}", + severity: actionlog.Notice, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// AttachmentErrGeneric returns "messaging:attachment.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func AttachmentErrGeneric(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrNotFound returns "messaging:attachment.notFound" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AttachmentErrNotFound(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "notFound", + action: "error", + message: "attachment not found", + log: "attachment not found", + severity: actionlog.Warning, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrChannelNotFound returns "messaging:attachment.channelNotFound" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AttachmentErrChannelNotFound(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "channelNotFound", + action: "error", + message: "channel not found", + log: "channel not found", + severity: actionlog.Warning, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrInvalidID returns "messaging:attachment.invalidID" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func AttachmentErrInvalidID(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "invalidID", + action: "error", + message: "invalid ID", + log: "invalid ID", + severity: actionlog.Warning, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrNotAllowedToListAttachments returns "messaging:attachment.notAllowedToListAttachments" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AttachmentErrNotAllowedToListAttachments(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "notAllowedToListAttachments", + action: "error", + message: "not allowed to list attachments", + log: "failed to list attachment; insufficient permissions", + severity: actionlog.Alert, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrNotAllowedToCreate returns "messaging:attachment.notAllowedToCreate" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AttachmentErrNotAllowedToCreate(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "notAllowedToCreate", + action: "error", + message: "not allowed to create attachments", + log: "failed to create attachment; insufficient permissions", + severity: actionlog.Alert, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrFailedToExtractMimeType returns "messaging:attachment.failedToExtractMimeType" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AttachmentErrFailedToExtractMimeType(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "failedToExtractMimeType", + action: "error", + message: "could not extract mime type", + log: "could not extract mime type", + severity: actionlog.Alert, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrFailedToStoreFile returns "messaging:attachment.failedToStoreFile" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AttachmentErrFailedToStoreFile(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "failedToStoreFile", + action: "error", + message: "could not extract store file", + log: "could not extract store file", + severity: actionlog.Alert, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrFailedToProcessImage returns "messaging:attachment.failedToProcessImage" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AttachmentErrFailedToProcessImage(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "failedToProcessImage", + action: "error", + message: "could not process image", + log: "could not process image", + severity: actionlog.Alert, + props: func() *attachmentActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// AttachmentErrNotAllowedToAttachToChannel returns "messaging:attachment.notAllowedToAttachToChannel" audit event as actionlog.Alert +// +// +// This function is auto-generated. +// +func AttachmentErrNotAllowedToAttachToChannel(props ...*attachmentActionProps) *attachmentError { + var e = &attachmentError{ + timestamp: time.Now(), + resource: "messaging:attachment", + error: "notAllowedToAttachToChannel", + action: "error", + message: "not allowed to attach files this channel", + log: "could not attach file to {channel}; insufficient permissions", + severity: actionlog.Alert, + props: func() *attachmentActionProps { + 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 attachmentAction 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 attachment) recordAction(ctx context.Context, props *attachmentActionProps, action func(...*attachmentActionProps) *attachmentAction, err error) error { + var ( + ok bool + + // Return error + retError *attachmentError + + // Recorder error + recError *attachmentError + ) + + if err != nil { + if retError, ok = err.(*attachmentError); !ok { + // got non-attachment error, wrap it with AttachmentErrGeneric + retError = AttachmentErrGeneric(props).Wrap(err) + + if action != nil { + // copy action to returning and recording error + retError.action = action().action + } + + // we'll use AttachmentErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + if action != 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 attachmentError + if unwrappedSinkError, ok := unwrappedError.(*attachmentError); 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/messaging/service/attachment_actions.yaml b/messaging/service/attachment_actions.yaml new file mode 100644 index 000000000..9740c61ea --- /dev/null +++ b/messaging/service/attachment_actions.yaml @@ -0,0 +1,79 @@ +# List of loggable service actions + +resource: messaging:attachment +service: attachment + +# Default sensitivity for actions +defaultActionSeverity: notice + +# default severity for errors +defaultErrorSeverity: alert + +import: + - github.com/cortezaproject/corteza-server/messaging/types + +props: + - name: messageID + type: uint64 + - name: replyTo + type: uint64 + - name: size + type: int64 + - name: name + - name: mimetype + - name: url + - name: attachment + type: "*types.Attachment" + fields: [ name, url, previewUrl, meta, userID, ID ] + - name: channel + type: "*types.Channel" + fields: [ name, topic, type, ID ] + +actions: + - action: search + log: "searched for attachments" + severity: info + + - action: lookup + log: "looked-up for a {attachment}" + severity: info + + - action: create + log: "created {attachment} on {channel}" + + - action: delete + log: "deleted {attachment} from {channel}" + +errors: + - error: notFound + message: "attachment not found" + severity: warning + + - error: channelNotFound + message: "channel not found" + severity: warning + + - error: invalidID + message: "invalid ID" + severity: warning + + - error: notAllowedToListAttachments + message: "not allowed to list attachments" + log: "failed to list attachment; insufficient permissions" + + - error: notAllowedToCreate + message: "not allowed to create attachments" + log: "failed to create attachment; insufficient permissions" + + - error: failedToExtractMimeType + message: "could not extract mime type" + + - error: failedToStoreFile + message: "could not extract store file" + + - error: failedToProcessImage + message: "could not process image" + + - error: notAllowedToAttachToChannel + message: "not allowed to attach files this channel" + log: "could not attach file to {channel}; insufficient permissions" diff --git a/messaging/service/channel.go b/messaging/service/channel.go index 92134b7e8..60dafd5b6 100644 --- a/messaging/service/channel.go +++ b/messaging/service/channel.go @@ -9,8 +9,8 @@ import ( "github.com/cortezaproject/corteza-server/messaging/repository" "github.com/cortezaproject/corteza-server/messaging/types" + "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/auth" - "github.com/cortezaproject/corteza-server/pkg/organization" ) type ( @@ -21,6 +21,8 @@ type ( event EventService ac applicationAccessController + actionlog actionlog.Recorder + channel repository.ChannelRepository cmember repository.ChannelMemberRepository unread repository.UnreadRepository @@ -92,6 +94,8 @@ func (svc *channel) With(ctx context.Context) ChannelService { event: Event(ctx), ac: DefaultAccessControl, + actionlog: DefaultActionlog, + channel: repository.Channel(ctx, db), cmember: repository.ChannelMember(ctx, db), unread: repository.Unread(ctx, db), @@ -117,9 +121,15 @@ func (svc *channel) FindByID(ID uint64) (ch *types.Channel, err error) { func (svc *channel) findByID(ID uint64) (ch *types.Channel, err error) { ch, err = svc.channel.FindByID(ID) if err != nil { - return - } else if err = svc.preloadExtras(types.ChannelSet{ch}); err != nil { - return + if repository.ErrChannelNotFound.Eq(err) { + return nil, ChannelErrNotFound() + } + + return nil, err + } + + if err = svc.preloadExtras(types.ChannelSet{ch}); err != nil { + return nil, err } return @@ -230,82 +240,81 @@ func (svc *channel) FindMembers(channelID uint64) (out types.ChannelMemberSet, e }) } -func (svc *channel) Create(in *types.Channel) (out *types.Channel, err error) { - if !in.Type.IsValid() { - return nil, errors.Errorf("invalid channel type") - } +func (svc *channel) Create(new *types.Channel) (ch *types.Channel, err error) { + var ( + aProps = &channelActionProps{changed: new} + ) - if len(in.Name) == 0 && in.Type != types.ChannelTypeGroup { - return nil, errors.New("channel name not provided") - } + err = svc.db.Transaction(func() (err error) { + if !new.Type.IsValid() { + return ChannelErrInvalidType() + } - if settingsChannelNameLength > 0 && len(in.Name) > settingsChannelNameLength { - return nil, errors.Errorf("channel name (%d characters) too long (max: %d)", len(in.Name), settingsChannelNameLength) - } + if len(new.Name) == 0 && new.Type != types.ChannelTypeGroup { + return ChannelErrNameEmpty() + } - if len(in.Topic) > 0 && settingsChannelTopicLength > 0 && len(in.Topic) > settingsChannelTopicLength { - return nil, errors.Errorf("channel topic (%d characters) too long (max: %d)", len(in.Topic), settingsChannelTopicLength) - } + if settingsChannelNameLength > 0 && len(new.Name) > settingsChannelNameLength { + return ChannelErrNameLength() + } - return out, svc.db.Transaction(func() (err error) { - var msg *types.Message - - var organisationID = organization.Corteza().ID + if len(new.Topic) > 0 && settingsChannelTopicLength > 0 && len(new.Topic) > settingsChannelTopicLength { + return ChannelErrTopicLength() + } var chCreatorID = auth.GetIdentityFromContext(svc.ctx).Identity() - mm := svc.buildMemberSet(chCreatorID, in.Members...) + mm := svc.buildMemberSet(chCreatorID, new.Members...) - if in.Type == types.ChannelTypeGroup { - if out, err = svc.checkGroupExistance(mm); err != nil { + if new.Type == types.ChannelTypeGroup { + if ch, err = svc.checkGroupExistance(mm); err != nil { return err - } else if out != nil && out.CanObserve { + } else if ch != nil && ch.CanObserve { // Group already exists so let's just return it return nil - } else if out != nil && !out.CanObserve { - return ErrNoPermissions.withStack() + } else if ch != nil && !ch.CanObserve { + return ChannelErrNotAllowedToRead() } } - if in.Type == types.ChannelTypePublic && !svc.ac.CanCreatePublicChannel(svc.ctx) { - return ErrNoPermissions.withStack() + if new.Type == types.ChannelTypePublic && !svc.ac.CanCreatePublicChannel(svc.ctx) { + return ChannelErrNotAllowedToCreate() } - if in.Type == types.ChannelTypePrivate && !svc.ac.CanCreatePrivateChannel(svc.ctx) { - return ErrNoPermissions.withStack() + if new.Type == types.ChannelTypePrivate && !svc.ac.CanCreatePrivateChannel(svc.ctx) { + return ChannelErrNotAllowedToCreate() } - if in.Type == types.ChannelTypeGroup && !svc.ac.CanCreateGroupChannel(svc.ctx) { - return ErrNoPermissions.withStack() + if new.Type == types.ChannelTypeGroup && !svc.ac.CanCreateGroupChannel(svc.ctx) { + return ChannelErrNotAllowedToCreate() } - if !in.MembershipPolicy.IsValid() { + if !new.MembershipPolicy.IsValid() { // Reset invalid membership flag to default - in.MembershipPolicy = types.ChannelMembershipPolicyDefault + new.MembershipPolicy = types.ChannelMembershipPolicyDefault } - if in.MembershipPolicy != types.ChannelMembershipPolicyDefault && !svc.ac.CanChangeChannelMembershipPolicy(svc.ctx, in) { - return ErrNoPermissions.withStack() + if new.MembershipPolicy != types.ChannelMembershipPolicyDefault && !svc.ac.CanChangeChannelMembershipPolicy(svc.ctx, new) { + return ChannelErrNotAllowedToCreate() } // This is a fresh channel, just copy values - out = &types.Channel{ - Name: in.Name, - Topic: in.Topic, - Type: in.Type, - MembershipPolicy: in.MembershipPolicy, - OrganisationID: organisationID, + ch = &types.Channel{ + Name: new.Name, + Topic: new.Topic, + Type: new.Type, + MembershipPolicy: new.MembershipPolicy, CreatorID: chCreatorID, } // Save the channel - if out, err = svc.channel.Create(out); err != nil { + if ch, err = svc.channel.Create(ch); err != nil { return } err = mm.Walk(func(m *types.ChannelMember) (err error) { // Assign channel ID to membership - m.ChannelID = out.ID + m.ChannelID = ch.ID // Create member if m, err = svc.createMember(m); err != nil { @@ -313,7 +322,7 @@ func (svc *channel) Create(in *types.Channel) (out *types.Channel, err error) { } // Subscribe all members - return svc.event.Join(m.UserID, out.ID) + return svc.event.Join(m.UserID, ch.ID) }) if err != nil { @@ -322,28 +331,24 @@ func (svc *channel) Create(in *types.Channel) (out *types.Channel, err error) { } // Copy all member IDs to channel's member slice - out.Members = mm.AllMemberIDs() + ch.Members = mm.AllMemberIDs() // Create the first message, doing this directly with repository to circumvent // message service constraints - if len(out.Name) == 0 { - svc.scheduleSystemMessage(out, `<@%d> created %s channel`, chCreatorID, out.Type) - } else if len(out.Topic) == 0 { - svc.scheduleSystemMessage(out, `<@%d> created %s channel **%s**`, chCreatorID, out.Type, out.Name) + if len(ch.Name) == 0 { + svc.scheduleSystemMessage(ch, `<@%d> created %s channel`, chCreatorID, ch.Type) + } else if len(ch.Topic) == 0 { + svc.scheduleSystemMessage(ch, `<@%d> created %s channel **%s**`, chCreatorID, ch.Type, ch.Name) } else { - svc.scheduleSystemMessage(out, `<@%d> created %s channel **%s**, topic: %s`, chCreatorID, out.Type, out.Name, out.Topic) + svc.scheduleSystemMessage(ch, `<@%d> created %s channel **%s**, topic: %s`, chCreatorID, ch.Type, ch.Name, ch.Topic) } - _ = msg - if err != nil { - // Message creation failed - return - } + _ = svc.flushSystemMessages() - svc.flushSystemMessages() - - return svc.sendChannelEvent(out) + return svc.sendChannelEvent(ch) }) + + return ch, svc.recordAction(svc.ctx, aProps, ChannelActionCreate, err) } func (svc *channel) buildMemberSet(owner uint64, members ...uint64) (mm types.ChannelMemberSet) { @@ -376,45 +381,54 @@ func (svc *channel) checkGroupExistance(mm types.ChannelMemberSet) (out *types.C return } -func (svc *channel) Update(in *types.Channel) (ch *types.Channel, err error) { - if in.ID == 0 { - return nil, ErrInvalidID.withStack() - } +func (svc *channel) Update(upd *types.Channel) (ch *types.Channel, err error) { + var ( + aProps = &channelActionProps{changed: upd} + ) - if len(in.Name) == 0 && in.Type != types.ChannelTypeGroup { - return nil, errors.New("channel name not provided") - } + err = svc.db.Transaction(func() (err error) { + if upd.ID == 0 { + return ChannelErrInvalidID() + } - if settingsChannelNameLength > 0 && len(in.Name) > settingsChannelNameLength { - return nil, errors.Errorf("channel name (%d characters) too long (max: %d)", len(in.Name), settingsChannelNameLength) - } + if !upd.Type.IsValid() { + return ChannelErrInvalidType() + } - if len(in.Topic) > 0 && settingsChannelTopicLength > 0 && len(in.Topic) > settingsChannelTopicLength { - return nil, errors.Errorf("channel topic (%d characters) too long (max: %d)", len(in.Topic), settingsChannelTopicLength) - } + if len(upd.Name) == 0 && upd.Type != types.ChannelTypeGroup { + return ChannelErrNameEmpty() + } - return ch, svc.db.Transaction(func() (err error) { + if settingsChannelNameLength > 0 && len(upd.Name) > settingsChannelNameLength { + return ChannelErrNameLength() + } + + if len(upd.Topic) > 0 && settingsChannelTopicLength > 0 && len(upd.Topic) > settingsChannelTopicLength { + return ChannelErrTopicLength() + } var changed bool - if ch, err = svc.FindByID(in.ID); err != nil { + if ch, err = svc.FindByID(upd.ID); err != nil { return } + aProps.setChannel(ch) + if !svc.ac.CanUpdateChannel(svc.ctx, ch) { - return ErrNoPermissions.withStack() + return ChannelErrNotAllowedToUpdate() } - if in.Type.IsValid() && ch.Type != in.Type { - if in.Type == types.ChannelTypePublic && !svc.ac.CanCreatePublicChannel(svc.ctx) { - return ErrNoPermissions.withStack() + if upd.Type.IsValid() && ch.Type != upd.Type { + if upd.Type == types.ChannelTypePublic && !svc.ac.CanCreatePublicChannel(svc.ctx) { + return ChannelErrNotAllowedToUpdate() } - if in.Type == types.ChannelTypePrivate && !svc.ac.CanCreatePrivateChannel(svc.ctx) { - return ErrNoPermissions.withStack() + if upd.Type == types.ChannelTypePrivate && !svc.ac.CanCreatePrivateChannel(svc.ctx) { + return ChannelErrNotAllowedToUpdate() } - if in.Type == types.ChannelTypeGroup && !svc.ac.CanCreateGroupChannel(svc.ctx) { - return ErrNoPermissions.withStack() + if upd.Type == types.ChannelTypeGroup && !svc.ac.CanCreateGroupChannel(svc.ctx) { + return ChannelErrNotAllowedToUpdate() } changed = true @@ -422,36 +436,36 @@ func (svc *channel) Update(in *types.Channel) (ch *types.Channel, err error) { var chUpdatorId = auth.GetIdentityFromContext(svc.ctx).Identity() - if len(in.Name) > 0 && ch.Name != in.Name { - if settingsChannelNameLength > 0 && len(in.Name) > settingsChannelNameLength { - return errors.Errorf("channel name (%d characters) too long (max: %d)", len(in.Name), settingsChannelNameLength) + if len(upd.Name) > 0 && ch.Name != upd.Name { + if settingsChannelNameLength > 0 && len(upd.Name) > settingsChannelNameLength { + return errors.Errorf("channel name (%d characters) too long (max: %d)", len(upd.Name), settingsChannelNameLength) } else if ch.Name != "" { - svc.scheduleSystemMessage(in, "<@%d> renamed channel **%s** (was: %s)", chUpdatorId, in.Name, ch.Name) + svc.scheduleSystemMessage(upd, "<@%d> renamed channel **%s** (was: %s)", chUpdatorId, upd.Name, ch.Name) } else { - svc.scheduleSystemMessage(in, "<@%d> set channel name to **%s**", chUpdatorId, in.Name) + svc.scheduleSystemMessage(upd, "<@%d> set channel name to **%s**", chUpdatorId, upd.Name) } - ch.Name = in.Name + ch.Name = upd.Name changed = true } - if len(in.Topic) > 0 && ch.Topic != in.Topic { - if settingsChannelTopicLength > 0 && len(in.Topic) > settingsChannelTopicLength { - return errors.Errorf("channel topic (%d characters) too long (max: %d)", len(in.Topic), settingsChannelTopicLength) + if len(upd.Topic) > 0 && ch.Topic != upd.Topic { + if settingsChannelTopicLength > 0 && len(upd.Topic) > settingsChannelTopicLength { + return errors.Errorf("channel topic (%d characters) too long (max: %d)", len(upd.Topic), settingsChannelTopicLength) } else if ch.Topic != "" { - svc.scheduleSystemMessage(in, "<@%d> changed channel topic: %s (was: %s)", chUpdatorId, in.Topic, ch.Topic) + svc.scheduleSystemMessage(upd, "<@%d> changed channel topic: %s (was: %s)", chUpdatorId, upd.Topic, ch.Topic) } else { - svc.scheduleSystemMessage(in, "<@%d> set channel topic to %s", chUpdatorId, in.Topic) + svc.scheduleSystemMessage(upd, "<@%d> set channel topic to %s", chUpdatorId, upd.Topic) } - ch.Topic = in.Topic + ch.Topic = upd.Topic changed = true } - if ch.MembershipPolicy != in.MembershipPolicy && !svc.ac.CanChangeChannelMembershipPolicy(svc.ctx, ch) { - return ErrNoPermissions.withStack() + if ch.MembershipPolicy != upd.MembershipPolicy && !svc.ac.CanChangeChannelMembershipPolicy(svc.ctx, ch) { + return ChannelErrNotAllowedToUpdate() } else { - ch.MembershipPolicy = in.MembershipPolicy + ch.MembershipPolicy = upd.MembershipPolicy changed = true } @@ -459,7 +473,7 @@ func (svc *channel) Update(in *types.Channel) (ch *types.Channel, err error) { return nil } // Save the updated channel - if ch, err = svc.channel.Update(in); err != nil { + if ch, err = svc.channel.Update(upd); err != nil { return } @@ -467,26 +481,35 @@ func (svc *channel) Update(in *types.Channel) (ch *types.Channel, err error) { return svc.sendChannelEvent(ch) }) + + return ch, svc.recordAction(svc.ctx, aProps, ChannelActionUpdate, err) + } func (svc *channel) Delete(ID uint64) (ch *types.Channel, err error) { - if ID == 0 { - return nil, ErrInvalidID.withStack() - } + var ( + aProps = &channelActionProps{} + ) + + err = svc.db.Transaction(func() (err error) { + if ID == 0 { + return ChannelErrInvalidID() + } - return ch, svc.db.Transaction(func() (err error) { var userID = auth.GetIdentityFromContext(svc.ctx).Identity() if ch, err = svc.findByID(ID); err != nil { return } + aProps.setChannel(ch) + if !svc.ac.CanDeleteChannel(svc.ctx, ch) { - return ErrNoPermissions.withStack() + return ChannelErrNotAllowedToDelete() } if ch.DeletedAt != nil { - return errors.New("channel already deleted") + return ChannelErrAlreadyDeleted() } else { now := time.Now() ch.DeletedAt = &now @@ -505,26 +528,34 @@ func (svc *channel) Delete(ID uint64) (ch *types.Channel, err error) { _ = svc.flushSystemMessages() return nil }) + + return ch, svc.recordAction(svc.ctx, aProps, ChannelActionDelete, err) } func (svc *channel) Undelete(ID uint64) (ch *types.Channel, err error) { - if ID == 0 { - return nil, ErrInvalidID.withStack() - } + var ( + aProps = &channelActionProps{} + ) + + err = svc.db.Transaction(func() (err error) { + if ID == 0 { + return ChannelErrInvalidID() + } - return ch, svc.db.Transaction(func() (err error) { var userID = auth.GetIdentityFromContext(svc.ctx).Identity() if ch, err = svc.findByID(ID); err != nil { return } + aProps.setChannel(ch) + if !svc.ac.CanUndeleteChannel(svc.ctx, ch) { - return ErrNoPermissions.withStack() + return ChannelErrNotAllowedToUndelete() } if ch.DeletedAt == nil { - return errors.New("channel not deleted") + return ChannelErrNotDeleted() } svc.scheduleSystemMessage(ch, "<@%d> undeleted this channel", userID) @@ -539,14 +570,19 @@ func (svc *channel) Undelete(ID uint64) (ch *types.Channel, err error) { svc.flushSystemMessages() return svc.sendChannelEvent(ch) }) + + return ch, svc.recordAction(svc.ctx, aProps, ChannelActionUndelete, err) } func (svc *channel) SetFlag(ID uint64, flag types.ChannelMembershipFlag) (ch *types.Channel, err error) { - if ID == 0 { - return nil, ErrInvalidID.withStack() - } + var ( + aProps = &channelActionProps{flag: string(flag)} + ) - return ch, svc.db.Transaction(func() (err error) { + err = svc.db.Transaction(func() (err error) { + if ID == 0 { + return ChannelErrInvalidID() + } var membership *types.ChannelMember var userID = auth.GetIdentityFromContext(svc.ctx).Identity() @@ -554,6 +590,8 @@ func (svc *channel) SetFlag(ID uint64, flag types.ChannelMembershipFlag) (ch *ty return } + aProps.setChannel(ch) + if members, err := svc.cmember.Find(types.ChannelMemberFilter{ChannelID: []uint64{ch.ID}, MemberID: []uint64{userID}}); err != nil { return err } else if len(members) == 1 { @@ -562,7 +600,7 @@ func (svc *channel) SetFlag(ID uint64, flag types.ChannelMembershipFlag) (ch *ty } if membership == nil { - return errors.New("not a member") + return ChannelErrNotMember() } if ch.Member, err = svc.cmember.Update(membership); err != nil { @@ -574,26 +612,34 @@ func (svc *channel) SetFlag(ID uint64, flag types.ChannelMembershipFlag) (ch *ty // Setting a flag on a channel is a private thing, // no need to send channel event back to everyone }) + + return ch, svc.recordAction(svc.ctx, aProps, ChannelActionSetFlag, err) + } func (svc *channel) Archive(ID uint64) (ch *types.Channel, err error) { - if ID == 0 { - return nil, ErrInvalidID.withStack() - } + var ( + aProps = &channelActionProps{} + ) - return ch, svc.db.Transaction(func() (err error) { + err = svc.db.Transaction(func() (err error) { + if ID == 0 { + return ChannelErrInvalidID() + } var userID = auth.GetIdentityFromContext(svc.ctx).Identity() if ch, err = svc.findByID(ID); err != nil { return } + aProps.setChannel(ch) + if !svc.ac.CanArchiveChannel(svc.ctx, ch) { - return ErrNoPermissions.withStack() + return ChannelErrNotAllowedToUndelete() } if ch.ArchivedAt != nil { - return errors.New("channel already archived") + return ChannelErrAlreadyArchived() } svc.scheduleSystemMessage(ch, "<@%d> archived this channel", userID) @@ -605,29 +651,36 @@ func (svc *channel) Archive(ID uint64) (ch *types.Channel, err error) { ch.ArchivedAt = timeNowPtr() } - svc.flushSystemMessages() + _ = svc.flushSystemMessages() return svc.sendChannelEvent(ch) }) + + return ch, svc.recordAction(svc.ctx, aProps, ChannelActionArchive, err) } func (svc *channel) Unarchive(ID uint64) (ch *types.Channel, err error) { - if ID == 0 { - return nil, ErrInvalidID.withStack() - } + var ( + aProps = &channelActionProps{} + ) - return ch, svc.db.Transaction(func() (err error) { + err = svc.db.Transaction(func() (err error) { + if ID == 0 { + return ChannelErrInvalidID() + } var userID = auth.GetIdentityFromContext(svc.ctx).Identity() if ch, err = svc.findByID(ID); err != nil { return } + aProps.setChannel(ch) + if !svc.ac.CanUnarchiveChannel(svc.ctx, ch) { return ErrNoPermissions.withStack() } if ch.ArchivedAt == nil { - return errors.New("channel not archived") + return ChannelErrNotArchived() } if err = svc.channel.UnarchiveByID(ID); err != nil { @@ -639,43 +692,51 @@ func (svc *channel) Unarchive(ID uint64) (ch *types.Channel, err error) { svc.scheduleSystemMessage(ch, "<@%d> unarchived this channel", userID) - svc.flushSystemMessages() + _ = svc.flushSystemMessages() return svc.sendChannelEvent(ch) }) + + return ch, svc.recordAction(svc.ctx, aProps, ChannelActionUnarchive, err) } func (svc *channel) InviteUser(channelID uint64, memberIDs ...uint64) (out types.ChannelMemberSet, err error) { - if channelID == 0 { - return nil, ErrInvalidID.withStack() - } - - for _, memberID := range memberIDs { - if memberID == 0 { - return nil, ErrInvalidID.withStack() - } - } - var ( - userID = auth.GetIdentityFromContext(svc.ctx).Identity() - ch *types.Channel - existing types.ChannelMemberSet + aProps = &channelActionProps{} ) - out = types.ChannelMemberSet{} + err = svc.db.Transaction(func() (err error) { + if channelID == 0 { + return ChannelErrInvalidID() + } - if ch, err = svc.FindByID(channelID); err != nil { - return - } + for _, memberID := range memberIDs { + if memberID == 0 { + return ChannelErrInvalidID() + } + } - if ch.Type == types.ChannelTypeGroup { - return nil, errors.New("adding members to a group is not currently supported") - } + var ( + userID = auth.GetIdentityFromContext(svc.ctx).Identity() + ch *types.Channel + existing types.ChannelMemberSet + ) - if !svc.ac.CanManageChannelMembers(svc.ctx, ch) { - return nil, ErrNoPermissions.withStack() - } + out = types.ChannelMemberSet{} + + if ch, err = svc.FindByID(channelID); err != nil { + return + } + + aProps.setChannel(ch) + + if ch.Type == types.ChannelTypeGroup { + return ChannelErrUnableToManageGroupMembers() + } + + if !svc.ac.CanManageChannelMembers(svc.ctx, ch) { + return ChannelErrNotAllowedToManageMembers() + } - return out, svc.db.Transaction(func() (err error) { if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil { return } @@ -704,36 +765,44 @@ func (svc *channel) InviteUser(channelID uint64, memberIDs ...uint64) (out types return svc.flushSystemMessages() }) + + return out, svc.recordAction(svc.ctx, aProps, ChannelActionInviteMember, err) } func (svc *channel) AddMember(channelID uint64, memberIDs ...uint64) (out types.ChannelMemberSet, err error) { - if channelID == 0 { - return nil, ErrInvalidID.withStack() - } - - for _, memberID := range memberIDs { - if memberID == 0 { - return nil, ErrInvalidID.withStack() - } - } - var ( - userID = auth.GetIdentityFromContext(svc.ctx).Identity() - ch *types.Channel - existing types.ChannelMemberSet + aProps = &channelActionProps{} ) - out = types.ChannelMemberSet{} + err = svc.db.Transaction(func() (err error) { + if channelID == 0 { + return ChannelErrInvalidID() + } - if ch, err = svc.FindByID(channelID); err != nil { - return - } + for _, memberID := range memberIDs { + if memberID == 0 { + return ChannelErrInvalidID() + } + } - if ch.Type == types.ChannelTypeGroup { - return nil, errors.New("adding members to a group is not currently supported") - } + var ( + userID = auth.GetIdentityFromContext(svc.ctx).Identity() + ch *types.Channel + existing types.ChannelMemberSet + ) + + out = types.ChannelMemberSet{} + + if ch, err = svc.FindByID(channelID); err != nil { + return + } + + aProps.setChannel(ch) + + if ch.Type == types.ChannelTypeGroup { + return ChannelErrUnableToManageGroupMembers() + } - return out, svc.db.Transaction(func() (err error) { if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil { return } @@ -751,9 +820,9 @@ func (svc *channel) AddMember(channelID uint64, memberIDs ...uint64) (out types. } if memberID == userID && !svc.ac.CanJoinChannel(svc.ctx, ch) { - return ErrNoPermissions.withStack() + return ChannelErrNotAllowedToJoin() } else if memberID != userID && !svc.ac.CanManageChannelMembers(svc.ctx, ch) { - return ErrNoPermissions.withStack() + return ChannelErrNotAllowedToManageMembers() } if !exists { @@ -776,12 +845,12 @@ func (svc *channel) AddMember(channelID uint64, memberIDs ...uint64) (out types. member, err = svc.createMember(member) } - svc.event.Join(memberID, channelID) - if err != nil { return err } + svc.event.Join(memberID, channelID) + out = append(out, member) } @@ -792,6 +861,8 @@ func (svc *channel) AddMember(channelID uint64, memberIDs ...uint64) (out types. return svc.flushSystemMessages() }) + + return out, svc.recordAction(svc.ctx, aProps, ChannelActionAddMember, err) } // createMember orchestrates member creation @@ -810,20 +881,26 @@ func (svc channel) createMember(member *types.ChannelMember) (m *types.ChannelMe func (svc *channel) DeleteMember(channelID uint64, memberIDs ...uint64) (err error) { var ( - userID = auth.GetIdentityFromContext(svc.ctx).Identity() - ch *types.Channel - existing types.ChannelMemberSet + aProps = &channelActionProps{} ) - if ch, err = svc.FindByID(channelID); err != nil { - return - } + err = svc.db.Transaction(func() (err error) { + var ( + userID = auth.GetIdentityFromContext(svc.ctx).Identity() + ch *types.Channel + existing types.ChannelMemberSet + ) - if ch.Type == types.ChannelTypeGroup { - return errors.New("removing members from a group is currently not supported") - } + if ch, err = svc.FindByID(channelID); err != nil { + return + } + + aProps.setChannel(ch) + + if ch.Type == types.ChannelTypeGroup { + return ChannelErrUnableToManageGroupMembers() + } - return svc.db.Transaction(func() (err error) { if existing, err = svc.cmember.Find(types.ChannelMemberFilterChannels(channelID)); err != nil { return } @@ -835,9 +912,9 @@ func (svc *channel) DeleteMember(channelID uint64, memberIDs ...uint64) (err err } if memberID == userID && !svc.ac.CanLeaveChannel(svc.ctx, ch) { - return ErrNoPermissions.withStack() + return ChannelErrNotAllowedToPart() } else if memberID != userID && !svc.ac.CanManageChannelMembers(svc.ctx, ch) { - return ErrNoPermissions.withStack() + return ChannelErrNotAllowedToManageMembers() } if userID == memberID { @@ -855,6 +932,9 @@ func (svc *channel) DeleteMember(channelID uint64, memberIDs ...uint64) (err err return svc.flushSystemMessages() }) + + return svc.recordAction(svc.ctx, aProps, ChannelActionRemoveMember, err) + } func (svc *channel) scheduleSystemMessage(ch *types.Channel, format string, a ...interface{}) { diff --git a/messaging/service/channel_actions.gen.go b/messaging/service/channel_actions.gen.go new file mode 100644 index 000000000..53ac3258a --- /dev/null +++ b/messaging/service/channel_actions.gen.go @@ -0,0 +1,1323 @@ +package service + +// This file is auto-generated from messaging/service/channel_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/messaging/types" + "github.com/cortezaproject/corteza-server/pkg/actionlog" +) + +type ( + channelActionProps struct { + channel *types.Channel + changed *types.Channel + filter *types.ChannelFilter + flag string + memberID uint64 + } + + channelAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *channelActionProps + } + + channelError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *channelActionProps + } +) + +var ( + // just a placeholder to cover template cases w/o fmt package use + _ = fmt.Println +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setChannel updates channelActionProps's channel +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *channelActionProps) setChannel(channel *types.Channel) *channelActionProps { + p.channel = channel + return p +} + +// setChanged updates channelActionProps's changed +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *channelActionProps) setChanged(changed *types.Channel) *channelActionProps { + p.changed = changed + return p +} + +// setFilter updates channelActionProps's filter +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *channelActionProps) setFilter(filter *types.ChannelFilter) *channelActionProps { + p.filter = filter + return p +} + +// setFlag updates channelActionProps's flag +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *channelActionProps) setFlag(flag string) *channelActionProps { + p.flag = flag + return p +} + +// setMemberID updates channelActionProps's memberID +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *channelActionProps) setMemberID(memberID uint64) *channelActionProps { + p.memberID = memberID + return p +} + +// serialize converts channelActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p channelActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + ) + + if p.channel != nil { + m.Set("channel.name", p.channel.Name, true) + m.Set("channel.topic", p.channel.Topic, true) + m.Set("channel.type", p.channel.Type, true) + m.Set("channel.ID", p.channel.ID, true) + } + if p.changed != nil { + m.Set("changed.name", p.changed.Name, true) + m.Set("changed.topic", p.changed.Topic, true) + m.Set("changed.type", p.changed.Type, true) + m.Set("changed.ID", p.changed.ID, true) + m.Set("changed.meta", p.changed.Meta, true) + } + if p.filter != nil { + m.Set("filter.query", p.filter.Query, true) + m.Set("filter.channelID", p.filter.ChannelID, true) + m.Set("filter.currentUserID", p.filter.CurrentUserID, true) + m.Set("filter.includeDeleted", p.filter.IncludeDeleted, true) + m.Set("filter.sort", p.filter.Sort, true) + } + m.Set("flag", p.flag, true) + m.Set("memberID", p.memberID, true) + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p channelActionProps) tr(in string, err error) string { + var ( + pairs = []string{"{err}"} + // first non-empty string + fns = func(ii ...interface{}) string { + for _, i := range ii { + if s := fmt.Sprintf("%v", i); len(s) > 0 { + return s + } + } + + return "" + } + ) + + 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.channel != nil { + // replacement for "{channel}" (in order how fields are defined) + pairs = append( + pairs, + "{channel}", + fns( + p.channel.Name, + p.channel.Topic, + p.channel.Type, + p.channel.ID, + ), + ) + pairs = append(pairs, "{channel.name}", fns(p.channel.Name)) + pairs = append(pairs, "{channel.topic}", fns(p.channel.Topic)) + pairs = append(pairs, "{channel.type}", fns(p.channel.Type)) + pairs = append(pairs, "{channel.ID}", fns(p.channel.ID)) + } + + if p.changed != nil { + // replacement for "{changed}" (in order how fields are defined) + pairs = append( + pairs, + "{changed}", + fns( + p.changed.Name, + p.changed.Topic, + p.changed.Type, + p.changed.ID, + p.changed.Meta, + ), + ) + pairs = append(pairs, "{changed.name}", fns(p.changed.Name)) + pairs = append(pairs, "{changed.topic}", fns(p.changed.Topic)) + pairs = append(pairs, "{changed.type}", fns(p.changed.Type)) + pairs = append(pairs, "{changed.ID}", fns(p.changed.ID)) + pairs = append(pairs, "{changed.meta}", fns(p.changed.Meta)) + } + + if p.filter != nil { + // replacement for "{filter}" (in order how fields are defined) + pairs = append( + pairs, + "{filter}", + fns( + p.filter.Query, + p.filter.ChannelID, + p.filter.CurrentUserID, + p.filter.IncludeDeleted, + p.filter.Sort, + ), + ) + pairs = append(pairs, "{filter.query}", fns(p.filter.Query)) + pairs = append(pairs, "{filter.channelID}", fns(p.filter.ChannelID)) + pairs = append(pairs, "{filter.currentUserID}", fns(p.filter.CurrentUserID)) + pairs = append(pairs, "{filter.includeDeleted}", fns(p.filter.IncludeDeleted)) + pairs = append(pairs, "{filter.sort}", fns(p.filter.Sort)) + } + pairs = append(pairs, "{flag}", fns(p.flag)) + pairs = append(pairs, "{memberID}", fns(p.memberID)) + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *channelAction) String() string { + var props = &channelActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *channelAction) 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 *channelError) String() string { + var props = &channelActionProps{} + + 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 *channelError) Error() string { + var props = &channelActionProps{} + + 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 *channelError) Is(Resource error) bool { + t, ok := Resource.(*channelError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps channelError around another error +// +// This function is auto-generated. +// +func (e *channelError) Wrap(err error) *channelError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *channelError) Unwrap() error { + return e.wrap +} + +func (e *channelError) 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 + +// ChannelActionCreate returns "messaging:channel.create" error +// +// This function is auto-generated. +// +func ChannelActionCreate(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "create", + log: "created {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionUpdate returns "messaging:channel.update" error +// +// This function is auto-generated. +// +func ChannelActionUpdate(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "update", + log: "updated {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionDelete returns "messaging:channel.delete" error +// +// This function is auto-generated. +// +func ChannelActionDelete(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "delete", + log: "deleted {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionUndelete returns "messaging:channel.undelete" error +// +// This function is auto-generated. +// +func ChannelActionUndelete(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "undelete", + log: "undeleted {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionArchive returns "messaging:channel.archive" error +// +// This function is auto-generated. +// +func ChannelActionArchive(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "archive", + log: "archived {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionUnarchive returns "messaging:channel.unarchive" error +// +// This function is auto-generated. +// +func ChannelActionUnarchive(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "unarchive", + log: "unarchived {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionSetFlag returns "messaging:channel.setFlag" error +// +// This function is auto-generated. +// +func ChannelActionSetFlag(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "setFlag", + log: "set flag {flag} on {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionInviteMember returns "messaging:channel.inviteMember" error +// +// This function is auto-generated. +// +func ChannelActionInviteMember(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "inviteMember", + log: "member {memberID} invited to {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionRemoveMember returns "messaging:channel.removeMember" error +// +// This function is auto-generated. +// +func ChannelActionRemoveMember(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "removeMember", + log: "member {memberID} removed from {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ChannelActionAddMember returns "messaging:channel.addMember" error +// +// This function is auto-generated. +// +func ChannelActionAddMember(props ...*channelActionProps) *channelAction { + a := &channelAction{ + timestamp: time.Now(), + resource: "messaging:channel", + action: "addMember", + log: "member {memberID} added to {channel}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// ChannelErrGeneric returns "messaging:channel.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrGeneric(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotFound returns "messaging:channel.notFound" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func ChannelErrNotFound(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notFound", + action: "error", + message: "channel does not exist", + log: "channel does not exist", + severity: actionlog.Warning, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrInvalidID returns "messaging:channel.invalidID" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrInvalidID(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "invalidID", + action: "error", + message: "invalid ID", + log: "invalid ID", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrInvalidType returns "messaging:channel.invalidType" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrInvalidType(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "invalidType", + action: "error", + message: "invalid type", + log: "invalid type", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNameLength returns "messaging:channel.nameLength" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNameLength(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "nameLength", + action: "error", + message: "name too long", + log: "name too long", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNameEmpty returns "messaging:channel.nameEmpty" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNameEmpty(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "nameEmpty", + action: "error", + message: "name not set", + log: "name not set", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrTopicLength returns "messaging:channel.topicLength" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrTopicLength(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "topicLength", + action: "error", + message: "topic too long", + log: "topic too long", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrAlreadyDeleted returns "messaging:channel.alreadyDeleted" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrAlreadyDeleted(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "alreadyDeleted", + action: "error", + message: "channel already deleted", + log: "channel already deleted", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotDeleted returns "messaging:channel.notDeleted" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotDeleted(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notDeleted", + action: "error", + message: "channel is not deleted", + log: "channel is not deleted", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrAlreadyArchived returns "messaging:channel.alreadyArchived" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrAlreadyArchived(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "alreadyArchived", + action: "error", + message: "channel already archived", + log: "channel already archived", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotArchived returns "messaging:channel.notArchived" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotArchived(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notArchived", + action: "error", + message: "channel is not archived", + log: "channel is not archived", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotMember returns "messaging:channel.notMember" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotMember(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notMember", + action: "error", + message: "not a member of this channel", + log: "not a member of this channel", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrUnableToManageGroupMembers returns "messaging:channel.unableToManageGroupMembers" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrUnableToManageGroupMembers(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "unableToManageGroupMembers", + action: "error", + message: "channel already deleted", + log: "channel already deleted", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToRead returns "messaging:channel.notAllowedToRead" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToRead(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToRead", + action: "error", + message: "not allowed to read this channel", + log: "could not read {channel}; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToListChannels returns "messaging:channel.notAllowedToListChannels" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToListChannels(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToListChannels", + action: "error", + message: "not allowed to list this channels", + log: "could not list channels; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToCreate returns "messaging:channel.notAllowedToCreate" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToCreate(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToCreate", + action: "error", + message: "not allowed to create channels", + log: "could not create channels; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToUpdate returns "messaging:channel.notAllowedToUpdate" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToUpdate(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToUpdate", + action: "error", + message: "not allowed to update this channel", + log: "could not update {channel}; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToJoin returns "messaging:channel.notAllowedToJoin" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToJoin(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToJoin", + action: "error", + message: "not allowed to join this channel", + log: "could not join {channel}; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToPart returns "messaging:channel.notAllowedToPart" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToPart(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToPart", + action: "error", + message: "not allowed to part this channel", + log: "could not part {channel}; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToDelete returns "messaging:channel.notAllowedToDelete" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToDelete(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToDelete", + action: "error", + message: "not allowed to delete this channel", + log: "could not delete {channel}; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToUndelete returns "messaging:channel.notAllowedToUndelete" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToUndelete(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToUndelete", + action: "error", + message: "not allowed to undelete this channel", + log: "could not undelete {channel}; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ChannelErrNotAllowedToManageMembers returns "messaging:channel.notAllowedToManageMembers" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ChannelErrNotAllowedToManageMembers(props ...*channelActionProps) *channelError { + var e = &channelError{ + timestamp: time.Now(), + resource: "messaging:channel", + error: "notAllowedToManageMembers", + action: "error", + message: "not allowed to manage channel members", + log: "could not manage channel members; insufficient permissions", + severity: actionlog.Error, + props: func() *channelActionProps { + 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 channelAction 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 channel) recordAction(ctx context.Context, props *channelActionProps, action func(...*channelActionProps) *channelAction, err error) error { + var ( + ok bool + + // Return error + retError *channelError + + // Recorder error + recError *channelError + ) + + if err != nil { + if retError, ok = err.(*channelError); !ok { + // got non-channel error, wrap it with ChannelErrGeneric + retError = ChannelErrGeneric(props).Wrap(err) + + if action != nil { + // copy action to returning and recording error + retError.action = action().action + } + + // we'll use ChannelErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + if action != 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 channelError + if unwrappedSinkError, ok := unwrappedError.(*channelError); 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/messaging/service/channel_actions.yaml b/messaging/service/channel_actions.yaml new file mode 100644 index 000000000..b99394ab5 --- /dev/null +++ b/messaging/service/channel_actions.yaml @@ -0,0 +1,133 @@ +# List of loggable service actions + +resource: messaging:channel +service: channel + +# Default sensitivity for actions +defaultActionSeverity: info + +# default severity for errors +defaultErrorSeverity: error + +import: + - github.com/cortezaproject/corteza-server/messaging/types + +props: + - name: channel + type: "*types.Channel" + fields: [ name, topic, type, ID ] + - name: changed + type: "*types.Channel" + fields: [ name, topic, type, ID, meta ] + - name: filter + type: "*types.ChannelFilter" + fields: [ query, channelID, currentUserID, includeDeleted, sort ] + - name: flag + - name: memberID + type: uint64 + +actions: + - action: create + log: "created {channel}" + + - action: update + log: "updated {channel}" + + - action: delete + log: "deleted {channel}" + + - action: undelete + log: "undeleted {channel}" + + - action: archive + log: "archived {channel}" + + - action: unarchive + log: "unarchived {channel}" + + - action: setFlag + log: "set flag {flag} on {channel}" + + - action: inviteMember + log: "member {memberID} invited to {channel}" + + - action: removeMember + log: "member {memberID} removed from {channel}" + + - action: addMember + log: "member {memberID} added to {channel}" + + +errors: + - error: notFound + message: "channel does not exist" + severity: warning + + - error: invalidID + message: "invalid ID" + + - error: invalidType + message: "invalid type" + + - error: nameLength + message: "name too long" + + - error: nameEmpty + message: "name not set" + + - error: topicLength + message: "topic too long" + + - error: alreadyDeleted + message: "channel already deleted" + + - error: notDeleted + message: "channel is not deleted" + + - error: alreadyArchived + message: "channel already archived" + + - error: notArchived + message: "channel is not archived" + + - error: notMember + message: "not a member of this channel" + + - error: unableToManageGroupMembers + message: "channel already deleted" + + - error: notAllowedToRead + message: "not allowed to read this channel" + log: "could not read {channel}; insufficient permissions" + + - error: notAllowedToListChannels + message: "not allowed to list this channels" + log: "could not list channels; insufficient permissions" + + - error: notAllowedToCreate + message: "not allowed to create channels" + log: "could not create channels; insufficient permissions" + + - error: notAllowedToUpdate + message: "not allowed to update this channel" + log: "could not update {channel}; insufficient permissions" + + - error: notAllowedToJoin + message: "not allowed to join this channel" + log: "could not join {channel}; insufficient permissions" + + - error: notAllowedToPart + message: "not allowed to part this channel" + log: "could not part {channel}; insufficient permissions" + + - error: notAllowedToDelete + message: "not allowed to delete this channel" + log: "could not delete {channel}; insufficient permissions" + + - error: notAllowedToUndelete + message: "not allowed to undelete this channel" + log: "could not undelete {channel}; insufficient permissions" + + - error: notAllowedToManageMembers + message: "not allowed to manage channel members" + log: "could not manage channel members; insufficient permissions" diff --git a/messaging/service/channel_test.go b/messaging/service/channel_test.go deleted file mode 100644 index 0614e992f..000000000 --- a/messaging/service/channel_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package service - -import ( - "strings" - "testing" - - "github.com/cortezaproject/corteza-server/messaging/types" - "github.com/stretchr/testify/require" -) - -func TestChannelNameTooShort(t *testing.T) { - svc := channel{} - e := func(out *types.Channel, err error) error { return err } - - require.True(t, e(svc.Create(&types.Channel{})) != nil, "Should not allow to create unnamed channels") - - if settingsChannelNameLength > 0 { - longName := strings.Repeat("X", settingsChannelNameLength+1) - require.True(t, e(svc.Create(&types.Channel{Name: longName})) != nil, "Should not allow to create channel with really long name") - } -} diff --git a/messaging/service/command.go b/messaging/service/command.go index 7574cda42..a43e611aa 100644 --- a/messaging/service/command.go +++ b/messaging/service/command.go @@ -3,22 +3,16 @@ package service import ( "context" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "github.com/cortezaproject/corteza-server/messaging/types" - "github.com/cortezaproject/corteza-server/pkg/logger" ) type ( command struct { - ctx context.Context - logger *zap.Logger + ctx context.Context } CommandService interface { With(context.Context) CommandService - Do(channelID uint64, command, input string) (*types.Message, error) } ) @@ -29,16 +23,10 @@ func Command(ctx context.Context) CommandService { func (svc command) With(ctx context.Context) CommandService { return &command{ - ctx: ctx, - logger: DefaultLogger.Named("command"), + ctx: ctx, } } -// log() returns zap's logger with requestID from current context and fields. -func (svc command) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(ctx, svc.logger).With(fields...) -} - func (svc command) Do(channelID uint64, command, input string) (*types.Message, error) { switch command { case "me": diff --git a/messaging/service/event.go b/messaging/service/event.go index 4b47322e0..058e64daf 100644 --- a/messaging/service/event.go +++ b/messaging/service/event.go @@ -3,12 +3,8 @@ package service import ( "context" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "github.com/cortezaproject/corteza-server/messaging/repository" "github.com/cortezaproject/corteza-server/messaging/types" - "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/cortezaproject/corteza-server/pkg/payload/outgoing" ) @@ -16,8 +12,6 @@ import ( type ( event struct { ctx context.Context - logger *zap.Logger - events repository.EventsRepository } @@ -35,25 +29,16 @@ type ( // Event sends sends events back to all (or specific) subscribers func Event(ctx context.Context) EventService { - return (&event{ - logger: DefaultLogger.Named("event"), - }).With(ctx) + return (&event{}).With(ctx) } func (svc event) With(ctx context.Context) EventService { return &event{ ctx: ctx, - logger: svc.logger, - events: repository.Events(), } } -// log() returns zap's logger with requestID from current context and fields. -func (svc event) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(ctx, svc.logger).With(fields...) -} - // Message sends message events to subscribers func (svc event) Message(m *types.Message) error { return svc.push(payload.Message(svc.ctx, m), types.EventQueueItemSubTypeChannel, m.ChannelID) diff --git a/messaging/service/message.go b/messaging/service/message.go index 647fe7534..99a011e77 100644 --- a/messaging/service/message.go +++ b/messaging/service/message.go @@ -7,22 +7,18 @@ import ( "strings" "github.com/pkg/errors" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" "github.com/cortezaproject/corteza-server/messaging/repository" "github.com/cortezaproject/corteza-server/messaging/types" "github.com/cortezaproject/corteza-server/pkg/auth" - "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/payload" ) type ( message struct { - db db - ctx context.Context - logger *zap.Logger - ac messageAccessController + db db + ctx context.Context + ac messageAccessController channel ChannelService @@ -82,8 +78,6 @@ var ( func Message(ctx context.Context) MessageService { return (&message{ - logger: DefaultLogger.Named("message"), - ac: DefaultAccessControl, channel: DefaultChannel, }).With(ctx) @@ -92,10 +86,8 @@ func Message(ctx context.Context) MessageService { func (svc message) With(ctx context.Context) MessageService { db := repository.DB(ctx) return &message{ - db: db, - ctx: ctx, - logger: svc.logger, - + db: db, + ctx: ctx, ac: svc.ac, channel: svc.channel, @@ -110,11 +102,6 @@ func (svc message) With(ctx context.Context) MessageService { } } -// log() returns zap's logger with requestID from current context and fields. -func (svc message) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { - return logger.AddRequestID(ctx, svc.logger).With(fields...) -} - func (svc message) Find(filter types.MessageFilter) (mm types.MessageSet, f types.MessageFilter, err error) { f = filter f.CurrentUserID = auth.GetIdentityFromContext(svc.ctx).Identity() @@ -758,7 +745,6 @@ func (svc message) countUnreads(ch *types.Channel, m *types.Message, userID uint if m.DeletedAt != nil { // When deleting message, all existing counters are decreased! if err = svc.unread.Dec(m.ChannelID, m.ReplyTo, m.UserID); err != nil { - svc.logger.With(zap.Error(err)).Info("could not decrement unread counter") return } } else if m.UpdatedAt == nil { @@ -773,7 +759,6 @@ func (svc message) countUnreads(ch *types.Channel, m *types.Message, userID uint // When new message is created, update all existing counters if err = svc.unread.Inc(m.ChannelID, m.ReplyTo, m.UserID); err != nil { - svc.logger.With(zap.Error(err)).Info("could not increment unread counter") return } } @@ -785,7 +770,6 @@ func (svc message) countUnreads(ch *types.Channel, m *types.Message, userID uint uuBase, err = svc.unread.Count(userID, ch.ID, threadIDs...) if err != nil { - svc.logger.With(zap.Error(err)).Info("could not count unread messages") return } @@ -794,7 +778,6 @@ func (svc message) countUnreads(ch *types.Channel, m *types.Message, userID uint // Do another count for channel uuChannels, err = svc.unread.Count(userID, ch.ID) if err != nil { - svc.logger.With(zap.Error(err)).Info("could not count unread messages") return } @@ -803,7 +786,6 @@ func (svc message) countUnreads(ch *types.Channel, m *types.Message, userID uint // Now recount all threads for this channel uuThreads, err = svc.unread.CountThreads(userID, ch.ID) if err != nil { - svc.logger.With(zap.Error(err)).Info("could not count unread messages") return } @@ -813,7 +795,6 @@ func (svc message) countUnreads(ch *types.Channel, m *types.Message, userID uint // This is a reply, make sure we fetch the new stats about unread replies and push them to users err = svc.event.UnreadCounters(uuBase) if err != nil { - svc.logger.With(zap.Error(err)).Info("could not send unread count event") return } } diff --git a/messaging/service/service.go b/messaging/service/service.go index 95937f6dd..ac3875401 100644 --- a/messaging/service/service.go +++ b/messaging/service/service.go @@ -8,6 +8,8 @@ import ( "github.com/cortezaproject/corteza-server/messaging/repository" "github.com/cortezaproject/corteza-server/messaging/types" + "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/http" @@ -39,6 +41,8 @@ var ( DefaultLogger *zap.Logger + DefaultActionlog actionlog.Recorder + DefaultSettings settings.Service DefaultAccessControl *accessControl @@ -56,6 +60,13 @@ var ( func Initialize(ctx context.Context, log *zap.Logger, c Config) (err error) { DefaultLogger = log.Named("service") + DefaultActionlog = actionlog.NewService( + // will log directly to system schema for now + 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/tests/messaging/channel_attach_test.go b/tests/messaging/channel_attach_test.go index a8a7bcd71..2fd213818 100644 --- a/tests/messaging/channel_attach_test.go +++ b/tests/messaging/channel_attach_test.go @@ -42,7 +42,7 @@ func TestChannelAttachNotMember(t *testing.T) { ch := h.repoMakePrivateCh() h.apiChAttach(ch, []byte("NOPE")). - Assert(helpers.AssertError("messaging.service.NoPermissions")). + Assert(helpers.AssertError("not allowed to attach files this channel")). End() } diff --git a/tests/messaging/channel_create_test.go b/tests/messaging/channel_create_test.go index 9a8b5f046..55b5f9c62 100644 --- a/tests/messaging/channel_create_test.go +++ b/tests/messaging/channel_create_test.go @@ -38,7 +38,7 @@ func TestChannelCreateDenied(t *testing.T) { h.deny(types.MessagingPermissionResource, "channel.public.create") h.apiChPubCreate("should not be created"). - Assert(helpers.AssertError("messaging.service.NoPermissions")). + Assert(helpers.AssertError("not allowed to create channels")). End() } @@ -61,7 +61,7 @@ func TestChannelCreateWithShortName(t *testing.T) { h.apiChPubCreate(""). Status(http.StatusOK). - Assert(helpers.AssertError("channel name not provided")). + Assert(helpers.AssertError("name not set")). End() } @@ -71,7 +71,7 @@ func TestChannelCreateWithLongName(t *testing.T) { h.apiChPubCreate(strings.Repeat("X ", 1000)). Status(http.StatusOK). - Assert(helpers.AssertError("channel name (2000 characters) too long (max: 40)")). + Assert(helpers.AssertError("name too long")). End() } diff --git a/tests/messaging/channel_update_test.go b/tests/messaging/channel_update_test.go index ea8786772..fe24c3711 100644 --- a/tests/messaging/channel_update_test.go +++ b/tests/messaging/channel_update_test.go @@ -45,10 +45,11 @@ func TestChannelUpdateNonexistent(t *testing.T) { req := &request.ChannelUpdate{ ChannelID: factory.Sonyflake.NextID(), Name: "some name", + Type: "public", } h.chUpdate(req). - Assert(helpers.AssertError("messaging.repository.ChannelNotFound")). + Assert(helpers.AssertError("channel does not exist")). End() } @@ -63,7 +64,7 @@ func TestChannelUpdateDenied(t *testing.T) { req.Name = "Updated name" h.chUpdate(req). - Assert(helpers.AssertError("messaging.service.NoPermissions")). + Assert(helpers.AssertError("not allowed to update this channel")). End() }