diff --git a/system/service/application.go b/system/service/application.go index 76e246e33..a046ea661 100644 --- a/system/service/application.go +++ b/system/service/application.go @@ -5,6 +5,7 @@ import ( "github.com/titpetric/factory" + "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/permissions" "github.com/cortezaproject/corteza-server/pkg/rh" @@ -21,6 +22,8 @@ type ( ac applicationAccessController eventbus eventDispatcher + actionlog actionlog.Recorder + application repository.ApplicationRepository } @@ -64,71 +67,111 @@ func (svc *application) With(ctx context.Context) ApplicationService { ac: svc.ac, eventbus: svc.eventbus, + actionlog: DefaultActionlog, + application: repository.Application(ctx, db), } } func (svc *application) FindByID(ID uint64) (app *types.Application, err error) { - if ID == 0 { - return nil, ErrInvalidID.withStack() - } + var ( + aaProps = &applicationActionProps{application: &types.Application{ID: ID}} + ) - if app, err = svc.application.FindByID(ID); err != nil { - return nil, err - } + err = svc.db.Transaction(func() error { + if ID == 0 { + return ApplicationErrInvalidID() + } - if !svc.ac.CanReadApplication(svc.ctx, app) { - return nil, ErrNoPermissions.withStack() - } + if app, err = svc.application.FindByID(ID); err != nil { + return ApplicationErrInvalidID().Wrap(err) + } - return app, nil + aaProps.setApplication(app) + + if !svc.ac.CanReadApplication(svc.ctx, app) { + return ApplicationErrNotAllowedToRead() + } + + return nil + }) + + return app, svc.recordAction(svc.ctx, aaProps, ApplicationActionLookup, err) } -func (svc *application) Find(f types.ApplicationFilter) (types.ApplicationSet, types.ApplicationFilter, error) { - f.IsReadable = svc.ac.FilterReadableApplications(svc.ctx) +func (svc *application) Find(filter types.ApplicationFilter) (aa types.ApplicationSet, f types.ApplicationFilter, err error) { + var ( + aaProps = &applicationActionProps{filter: &filter} + ) - if f.Deleted > rh.FilterStateExcluded { - // If list with deleted applications is requested - // user must have access permissions to system (ie: is admin) - // - // not the best solution but ATM it allows us to have at least - // some kind of control over who can see deleted applications - if !svc.ac.CanAccess(svc.ctx) { - return nil, f, ErrNoPermissions.withStack() + err = svc.db.Transaction(func() error { + filter.IsReadable = svc.ac.FilterReadableApplications(svc.ctx) + + if filter.Deleted > rh.FilterStateExcluded { + // If list with deleted applications is requested + // user must have access permissions to system (ie: is admin) + // + // not the best solution but ATM it allows us to have at least + // some kind of control over who can see deleted applications + if !svc.ac.CanAccess(svc.ctx) { + return ApplicationErrNotAllowedToListApplications() + } } - } - return svc.application.Find(f) + aa, f, err = svc.application.Find(filter) + return err + }) + + return aa, f, svc.recordAction(svc.ctx, aaProps, ApplicationActionSearch, err) } func (svc *application) Create(new *types.Application) (app *types.Application, err error) { - if !svc.ac.CanCreateApplication(svc.ctx) { - return nil, ErrNoPermissions.withStack() - } + var ( + aaProps = &applicationActionProps{new: new} + ) - if err = svc.eventbus.WaitFor(svc.ctx, event.ApplicationBeforeCreate(new, nil)); err != nil { - return - } + err = svc.db.Transaction(func() (err error) { + if !svc.ac.CanCreateApplication(svc.ctx) { + return ApplicationErrNotAllowedToCreate() + } - if app, err = svc.application.Create(new); err != nil { - return - } + if err = svc.eventbus.WaitFor(svc.ctx, event.ApplicationBeforeCreate(new, nil)); err != nil { + return + } - defer svc.eventbus.Dispatch(svc.ctx, event.ApplicationAfterCreate(new, nil)) - return + if app, err = svc.application.Create(new); err != nil { + return + } + aaProps.setApplication(app) + + defer svc.eventbus.Dispatch(svc.ctx, event.ApplicationAfterCreate(new, nil)) + return nil + }) + + return app, svc.recordAction(svc.ctx, aaProps, ApplicationActionCreate, err) } func (svc *application) Update(upd *types.Application) (app *types.Application, err error) { - if !svc.ac.CanUpdateApplication(svc.ctx, upd) { - return nil, ErrNoPermissions.withStack() - } + var ( + aaProps = &applicationActionProps{update: upd} + ) + + err = svc.db.Transaction(func() (err error) { + if upd.ID == 0 { + return ApplicationErrInvalidID() + } - return app, svc.db.Transaction(func() (err error) { if app, err = svc.application.FindByID(upd.ID); err != nil { return } + aaProps.setApplication(app) + + if !svc.ac.CanUpdateApplication(svc.ctx, app) { + return ApplicationErrNotAllowedToUpdate() + } + if err = svc.eventbus.WaitFor(svc.ctx, event.ApplicationBeforeUpdate(upd, app)); err != nil { return } @@ -145,38 +188,76 @@ func (svc *application) Update(upd *types.Application) (app *types.Application, defer svc.eventbus.Dispatch(svc.ctx, event.ApplicationAfterUpdate(upd, app)) return nil }) + + return app, svc.recordAction(svc.ctx, aaProps, ApplicationActionUpdate, err) } func (svc *application) Delete(ID uint64) (err error) { var ( - app *types.Application + aaProps = &applicationActionProps{} + app *types.Application ) - if app, err = svc.application.FindByID(ID); err != nil { - return - } + err = svc.db.Transaction(func() (err error) { + if ID == 0 { + return ApplicationErrInvalidID() + } - if !svc.ac.CanDeleteApplication(svc.ctx, app) { - return ErrNoPermissions.withStack() - } - if err = svc.eventbus.WaitFor(svc.ctx, event.ApplicationBeforeDelete(nil, app)); err != nil { - return - } + if app, err = svc.application.FindByID(ID); err != nil { + return + } - if err = svc.application.DeleteByID(ID); err != nil { - return - } + if !svc.ac.CanDeleteApplication(svc.ctx, app) { + return ApplicationErrNotAllowedToDelete() + } - defer svc.eventbus.Dispatch(svc.ctx, event.ApplicationAfterDelete(nil, app)) - return + if err = svc.eventbus.WaitFor(svc.ctx, event.ApplicationBeforeDelete(nil, app)); err != nil { + return + } + + if err = svc.application.DeleteByID(ID); err != nil { + return + } + + defer svc.eventbus.Dispatch(svc.ctx, event.ApplicationAfterDelete(nil, app)) + return nil + }) + + return svc.recordAction(svc.ctx, aaProps, ApplicationActionDelete, err) } -func (svc *application) Undelete(ID uint64) error { - app := &types.Application{ID: ID} +func (svc *application) Undelete(ID uint64) (err error) { + var ( + aaProps = &applicationActionProps{} + app *types.Application + ) - if !svc.ac.CanDeleteApplication(svc.ctx, app) { - return ErrNoPermissions.withStack() - } + err = svc.db.Transaction(func() (err error) { + if ID == 0 { + return ApplicationErrInvalidID() + } - return svc.application.UndeleteByID(ID) + if app, err = svc.application.FindByID(ID); err != nil { + return + } + + if !svc.ac.CanDeleteApplication(svc.ctx, app) { + return ApplicationErrNotAllowedToUndelete() + } + + // @todo add event + // if err = svc.eventbus.WaitFor(svc.ctx, event.ApplicationBeforeUndelete(nil, app)); err != nil { + // return + // } + + if err = svc.application.UndeleteByID(ID); err != nil { + return + } + + // @todo add event + // defer svc.eventbus.Dispatch(svc.ctx, event.ApplicationAfterUndelete(nil, app)) + return nil + }) + + return svc.recordAction(svc.ctx, aaProps, ApplicationActionDelete, err) } diff --git a/system/service/application_actions.gen.go b/system/service/application_actions.gen.go new file mode 100644 index 000000000..48f370303 --- /dev/null +++ b/system/service/application_actions.gen.go @@ -0,0 +1,783 @@ +package service + +// This file is auto-generated from system/service/application_actions.yaml +// + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/cortezaproject/corteza-server/pkg/actionlog" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + applicationActionProps struct { + application *types.Application + new *types.Application + update *types.Application + filter *types.ApplicationFilter + } + + applicationAction struct { + timestamp time.Time + resource string + action string + log string + severity actionlog.Severity + + // prefix for error when action fails + errorMessage string + + props *applicationActionProps + } + + applicationError struct { + timestamp time.Time + error string + resource string + action string + message string + log string + severity actionlog.Severity + + wrap error + + props *applicationActionProps + } +) + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Props methods +// setApplication updates applicationActionProps's application +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *applicationActionProps) setApplication(application *types.Application) *applicationActionProps { + p.application = application + return p +} + +// setNew updates applicationActionProps's new +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *applicationActionProps) setNew(new *types.Application) *applicationActionProps { + p.new = new + return p +} + +// setUpdate updates applicationActionProps's update +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *applicationActionProps) setUpdate(update *types.Application) *applicationActionProps { + p.update = update + return p +} + +// setFilter updates applicationActionProps's filter +// +// Allows method chaining +// +// This function is auto-generated. +// +func (p *applicationActionProps) setFilter(filter *types.ApplicationFilter) *applicationActionProps { + p.filter = filter + return p +} + +// serialize converts applicationActionProps to actionlog.Meta +// +// This function is auto-generated. +// +func (p applicationActionProps) serialize() actionlog.Meta { + var ( + m = make(actionlog.Meta) + str = func(i interface{}) string { return fmt.Sprintf("%v", i) } + ) + + if p.application != nil { + m["application.name"] = str(p.application.Name) + m["application.ID"] = str(p.application.ID) + } + if p.new != nil { + m["new.name"] = str(p.new.Name) + m["new.ID"] = str(p.new.ID) + } + if p.update != nil { + m["update.name"] = str(p.update.Name) + m["update.ID"] = str(p.update.ID) + } + if p.filter != nil { + m["filter.query"] = str(p.filter.Query) + m["filter.name"] = str(p.filter.Name) + m["filter.deleted"] = str(p.filter.Deleted) + m["filter.sort"] = str(p.filter.Sort) + } + + return m +} + +// tr translates string and replaces meta value placeholder with values +// +// This function is auto-generated. +// +func (p applicationActionProps) tr(in string, err error) string { + var pairs = []string{"{err}"} + + if err != nil { + for { + // Unwrap errors + ue := errors.Unwrap(err) + if ue == nil { + break + } + + err = ue + } + + pairs = append(pairs, err.Error()) + } else { + pairs = append(pairs, "nil") + } + + if p.application != nil { + pairs = append(pairs, "{application}", fmt.Sprintf("%v", p.application.Name)) + pairs = append(pairs, "{application.name}", fmt.Sprintf("%v", p.application.Name)) + pairs = append(pairs, "{application.ID}", fmt.Sprintf("%v", p.application.ID)) + } + + if p.new != nil { + pairs = append(pairs, "{new}", fmt.Sprintf("%v", p.new.Name)) + pairs = append(pairs, "{new.name}", fmt.Sprintf("%v", p.new.Name)) + pairs = append(pairs, "{new.ID}", fmt.Sprintf("%v", p.new.ID)) + } + + if p.update != nil { + pairs = append(pairs, "{update}", fmt.Sprintf("%v", p.update.Name)) + pairs = append(pairs, "{update.name}", fmt.Sprintf("%v", p.update.Name)) + pairs = append(pairs, "{update.ID}", fmt.Sprintf("%v", p.update.ID)) + } + + if p.filter != nil { + pairs = append(pairs, "{filter}", fmt.Sprintf("%v", p.filter.Query)) + pairs = append(pairs, "{filter.query}", fmt.Sprintf("%v", p.filter.Query)) + pairs = append(pairs, "{filter.name}", fmt.Sprintf("%v", p.filter.Name)) + pairs = append(pairs, "{filter.deleted}", fmt.Sprintf("%v", p.filter.Deleted)) + pairs = append(pairs, "{filter.sort}", fmt.Sprintf("%v", p.filter.Sort)) + } + return strings.NewReplacer(pairs...).Replace(in) +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Action methods + +// String returns loggable description as string +// +// This function is auto-generated. +// +func (a *applicationAction) String() string { + var props = &applicationActionProps{} + + if a.props != nil { + props = a.props + } + + return props.tr(a.log, nil) +} + +func (e *applicationAction) 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 *applicationError) String() string { + var props = &applicationActionProps{} + + 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 *applicationError) Error() string { + var props = &applicationActionProps{} + + 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 *applicationError) Is(Resource error) bool { + t, ok := Resource.(*applicationError) + if !ok { + return false + } + + return t.resource == e.resource && t.error == e.error +} + +// Wrap wraps applicationError around another error +// +// This function is auto-generated. +// +func (e *applicationError) Wrap(err error) *applicationError { + e.wrap = err + return e +} + +// Unwrap returns wrapped error +// +// This function is auto-generated. +// +func (e *applicationError) Unwrap() error { + return e.wrap +} + +func (e *applicationError) 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 + +// ApplicationActionSearch returns "system:application.search" error +// +// This function is auto-generated. +// +func ApplicationActionSearch(props ...*applicationActionProps) *applicationAction { + a := &applicationAction{ + timestamp: time.Now(), + resource: "system:application", + action: "search", + log: "searched for applications", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ApplicationActionLookup returns "system:application.lookup" error +// +// This function is auto-generated. +// +func ApplicationActionLookup(props ...*applicationActionProps) *applicationAction { + a := &applicationAction{ + timestamp: time.Now(), + resource: "system:application", + action: "lookup", + log: "looked-up for a {application}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ApplicationActionCreate returns "system:application.create" error +// +// This function is auto-generated. +// +func ApplicationActionCreate(props ...*applicationActionProps) *applicationAction { + a := &applicationAction{ + timestamp: time.Now(), + resource: "system:application", + action: "create", + log: "created {application}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ApplicationActionUpdate returns "system:application.update" error +// +// This function is auto-generated. +// +func ApplicationActionUpdate(props ...*applicationActionProps) *applicationAction { + a := &applicationAction{ + timestamp: time.Now(), + resource: "system:application", + action: "update", + log: "updated {application}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ApplicationActionDelete returns "system:application.delete" error +// +// This function is auto-generated. +// +func ApplicationActionDelete(props ...*applicationActionProps) *applicationAction { + a := &applicationAction{ + timestamp: time.Now(), + resource: "system:application", + action: "delete", + log: "deleted {application}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ApplicationActionUndelete returns "system:application.undelete" error +// +// This function is auto-generated. +// +func ApplicationActionUndelete(props ...*applicationActionProps) *applicationAction { + a := &applicationAction{ + timestamp: time.Now(), + resource: "system:application", + action: "undelete", + log: "undeleted {application}", + severity: actionlog.Info, + } + + if len(props) > 0 { + a.props = props[0] + } + + return a +} + +// ********************************************************************************************************************* +// ********************************************************************************************************************* +// Error constructors + +// ApplicationErrGeneric returns "system:application.generic" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ApplicationErrGeneric(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "generic", + action: "error", + message: "failed to complete request due to internal error", + log: "{err}", + severity: actionlog.Error, + props: func() *applicationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ApplicationErrNonexistent returns "system:application.nonexistent" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func ApplicationErrNonexistent(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "nonexistent", + action: "error", + message: "application does not exist", + log: "application does not exist", + severity: actionlog.Warning, + props: func() *applicationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ApplicationErrInvalidID returns "system:application.invalidID" audit event as actionlog.Warning +// +// +// This function is auto-generated. +// +func ApplicationErrInvalidID(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "invalidID", + action: "error", + message: "invalid ID", + log: "invalid ID", + severity: actionlog.Warning, + props: func() *applicationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ApplicationErrNotAllowedToRead returns "system:application.notAllowedToRead" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ApplicationErrNotAllowedToRead(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "notAllowedToRead", + action: "error", + message: "not allowed to read application", + log: "failed to read {application.name}; insufficient permissions", + severity: actionlog.Error, + props: func() *applicationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ApplicationErrNotAllowedToListApplications returns "system:application.notAllowedToListApplications" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ApplicationErrNotAllowedToListApplications(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "notAllowedToListApplications", + action: "error", + message: "not allowed to list applications", + log: "failed to list application; insufficient permissions", + severity: actionlog.Error, + props: func() *applicationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ApplicationErrNotAllowedToCreate returns "system:application.notAllowedToCreate" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ApplicationErrNotAllowedToCreate(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "notAllowedToCreate", + action: "error", + message: "not allowed to create application", + log: "failed to create application; insufficient permissions", + severity: actionlog.Error, + props: func() *applicationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ApplicationErrNotAllowedToUpdate returns "system:application.notAllowedToUpdate" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ApplicationErrNotAllowedToUpdate(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "notAllowedToUpdate", + action: "error", + message: "not allowed to update application", + log: "failed to update {application.name}; insufficient permissions", + severity: actionlog.Error, + props: func() *applicationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ApplicationErrNotAllowedToDelete returns "system:application.notAllowedToDelete" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ApplicationErrNotAllowedToDelete(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "notAllowedToDelete", + action: "error", + message: "not allowed to delete application", + log: "failed to delete {application.name}; insufficient permissions", + severity: actionlog.Error, + props: func() *applicationActionProps { + if len(props) > 0 { + return props[0] + } + return nil + }(), + } + + if len(props) > 0 { + e.props = props[0] + } + + return e + +} + +// ApplicationErrNotAllowedToUndelete returns "system:application.notAllowedToUndelete" audit event as actionlog.Error +// +// +// This function is auto-generated. +// +func ApplicationErrNotAllowedToUndelete(props ...*applicationActionProps) *applicationError { + var e = &applicationError{ + timestamp: time.Now(), + resource: "system:application", + error: "notAllowedToUndelete", + action: "error", + message: "not allowed to undelete application", + log: "failed to undelete {application.name}; insufficient permissions", + severity: actionlog.Error, + props: func() *applicationActionProps { + 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 applicationAction 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 application) recordAction(ctx context.Context, props *applicationActionProps, action func(...*applicationActionProps) *applicationAction, err error) error { + var ( + ok bool + + // Return error + retError *applicationError + + // Recorder error + recError *applicationError + ) + + if err != nil { + if retError, ok = err.(*applicationError); !ok { + // got non-application error, wrap it with ApplicationErrGeneric + retError = ApplicationErrGeneric(props).Wrap(err) + + // copy action to returning and recording error + retError.action = action().action + + // we'll use ApplicationErrGeneric for recording too + // because it can hold more info + recError = retError + } else if retError != nil { + // copy action to returning and recording error + retError.action = action().action + // start with copy of return error for recording + // this will be updated with tha root cause as we try and + // unwrap the error + recError = retError + + // find the original recError for this error + // for the purpose of logging + var unwrappedError error = retError + for { + if unwrappedError = errors.Unwrap(unwrappedError); unwrappedError == nil { + // nothing wrapped + break + } + + // update recError ONLY of wrapped error is of type applicationError + if unwrappedSinkError, ok := unwrappedError.(*applicationError); ok { + recError = unwrappedSinkError + } + } + + if retError.props == nil { + // set props on returning error if empty + retError.props = props + } + + if recError.props == nil { + // set props on recording error if empty + recError.props = props + } + } + } + + if svc.actionlog != nil { + if retError != nil { + // failed action, log error + svc.actionlog.Record(ctx, recError) + } else if action != nil { + // successful + svc.actionlog.Record(ctx, action(props)) + } + } + + if err == nil { + // retError not an interface and that WILL (!!) cause issues + // with nil check (== nil) when it is not explicitly returned + return nil + } + + return retError +} diff --git a/system/service/application_actions.yaml b/system/service/application_actions.yaml new file mode 100644 index 000000000..1617d695c --- /dev/null +++ b/system/service/application_actions.yaml @@ -0,0 +1,81 @@ +# List of loggable service actions + +resource: system:application +service: application + +# Default sensitivity for actions +defaultActionSeverity: info + +# default severity for errors +defaultErrorSeverity: error + +import: + - github.com/cortezaproject/corteza-server/system/types + +props: + - name: application + type: "*types.Application" + fields: [ name, ID ] + - name: new + type: "*types.Application" + fields: [ name, ID ] + - name: update + type: "*types.Application" + fields: [ name, ID ] + - name: filter + type: "*types.ApplicationFilter" + fields: [ query, name, deleted, sort ] + +actions: + - action: search + log: "searched for applications" + severity: info + + - action: lookup + log: "looked-up for a {application}" + severity: info + + - action: create + log: "created {application}" + + - action: update + log: "updated {application}" + + - action: delete + log: "deleted {application}" + + - action: undelete + log: "undeleted {application}" + +errors: + - error: nonexistent + message: "application does not exist" + severity: warning + + - error: invalidID + message: "invalid ID" + severity: warning + + - error: notAllowedToRead + message: "not allowed to read application" + log: "failed to read {application.name}; insufficient permissions" + + - error: notAllowedToListApplications + message: "not allowed to list applications" + log: "failed to list application; insufficient permissions" + + - error: notAllowedToCreate + message: "not allowed to create application" + log: "failed to create application; insufficient permissions" + + - error: notAllowedToUpdate + message: "not allowed to update application" + log: "failed to update {application.name}; insufficient permissions" + + - error: notAllowedToDelete + message: "not allowed to delete application" + log: "failed to delete {application.name}; insufficient permissions" + + - error: notAllowedToUndelete + message: "not allowed to undelete application" + log: "failed to undelete {application.name}; insufficient permissions" diff --git a/tests/system/application_test.go b/tests/system/application_test.go index 413f6fc8a..c17712de9 100644 --- a/tests/system/application_test.go +++ b/tests/system/application_test.go @@ -80,7 +80,7 @@ func TestApplicationCreateForbidden(t *testing.T) { FormData("name", "my-app"). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoPermissions")). + Assert(helpers.AssertError("not allowed to create application")). End() } @@ -106,7 +106,7 @@ func TestApplicationUpdateForbidden(t *testing.T) { FormData("name", "changed-name"). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoPermissions")). + Assert(helpers.AssertError("not allowed to update application")). End() } @@ -137,7 +137,7 @@ func TestApplicationDeleteForbidden(t *testing.T) { Delete(fmt.Sprintf("/application/%d", a.ID)). Expect(t). Status(http.StatusOK). - Assert(helpers.AssertError("system.service.NoPermissions")). + Assert(helpers.AssertError("not allowed to delete application")). End() }