Implement service actions & errors on messaging

This commit is contained in:
Denis Arh
2020-05-31 19:23:43 +02:00
parent ebff2fd6a6
commit d0afb17cf3
21 changed files with 3289 additions and 370 deletions
+2 -11
View File
@@ -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{}
+4
View File
@@ -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)
}
+8 -12
View File
@@ -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)
}
+1 -1
View File
@@ -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,
+26 -3
View File
@@ -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
@@ -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
}
@@ -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"
+92 -72
View File
@@ -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)
}
}
+863
View File
@@ -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
}
+79
View File
@@ -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"
+270 -190
View File
@@ -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{}) {
File diff suppressed because it is too large Load Diff
+133
View File
@@ -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"
-21
View File
@@ -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")
}
}
+2 -14
View File
@@ -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":
+1 -16
View File
@@ -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)
+5 -24
View File
@@ -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
}
}
+11
View File
@@ -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
+1 -1
View File
@@ -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()
}
+3 -3
View File
@@ -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()
}
+3 -2
View File
@@ -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()
}