Implement service actions & errors on messaging
This commit is contained in:
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user