From eb1c1cf65f5777972dad7c21d6ab55d31440068b Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Sun, 25 Aug 2019 09:54:16 +0200 Subject: [PATCH] Add trigger management access-control, disable direct trigger management --- compose/internal/service/access_control.go | 2 +- compose/internal/service/automation_script.go | 115 ++++++++++++++---- compose/internal/service/error.go | 21 ++-- compose/internal/service/service.go | 5 +- compose/rest/automation_trigger.go | 72 ++++++----- pkg/automation/trigger.go | 16 +++ 6 files changed, 159 insertions(+), 72 deletions(-) diff --git a/compose/internal/service/access_control.go b/compose/internal/service/access_control.go index abff338eb..206ff29a3 100644 --- a/compose/internal/service/access_control.go +++ b/compose/internal/service/access_control.go @@ -114,7 +114,7 @@ func (svc accessControl) CanDeleteRecord(ctx context.Context, r *types.Module) b } func (svc accessControl) CanManageAutomationTriggersOnModule(ctx context.Context, r *types.Module) bool { - return svc.can(ctx, r, "automation-trigger.manage", permissions.Allowed) + return svc.can(ctx, r, "automation-trigger.manage") } func (svc accessControl) CanCreateChart(ctx context.Context, r *types.Namespace) bool { diff --git a/compose/internal/service/automation_script.go b/compose/internal/service/automation_script.go index 4746bca56..8a2af3a52 100644 --- a/compose/internal/service/automation_script.go +++ b/compose/internal/service/automation_script.go @@ -16,6 +16,7 @@ type ( logger *zap.Logger scriptManager automationScriptManager ns NamespaceService + mod ModuleService ac automationScriptAccessController } @@ -37,6 +38,8 @@ type ( CanReadAutomationScript(context.Context, *automation.Script) bool CanUpdateAutomationScript(context.Context, *automation.Script) bool CanDeleteAutomationScript(context.Context, *automation.Script) bool + + CanManageAutomationTriggersOnModule(context.Context, *types.Module) bool } automationScriptNamespaceFinder interface { @@ -49,7 +52,8 @@ func AutomationScript(sm automationScriptManager) automationScript { scriptManager: sm, logger: DefaultLogger.Named("automation-script"), ac: DefaultAccessControl, - ns: Namespace(), + mod: DefaultModule, + ns: DefaultNamespace, } return svc @@ -78,9 +82,13 @@ func (svc automationScript) Find(ctx context.Context, namespaceID uint64, f auto } func (svc automationScript) Create(ctx context.Context, namespaceID uint64, mod *automation.Script) (err error) { - if ns, _, err := svc.loadCombo(ctx, namespaceID, 0); err != nil { + var ns *types.Namespace + + if ns, _, err = svc.loadCombo(ctx, namespaceID, 0); err != nil { return err - } else if !svc.ac.CanCreateAutomationScript(ctx, ns) { + } + + if !svc.ac.CanCreateAutomationScript(ctx, ns) { return ErrNoCreatePermissions.withStack() } @@ -90,35 +98,57 @@ func (svc automationScript) Create(ctx context.Context, namespaceID uint64, mod } } + err = mod.Triggers().Walk(func(t *automation.Trigger) error { + return svc.isValidTrigger(ctx, t, namespaceID) + }) + + if err != nil { + return + } + return svc.scriptManager.CreateScript(ctx, mod) } func (svc automationScript) Update(ctx context.Context, namespaceID uint64, mod *automation.Script) (err error) { - if _, s, err := svc.loadCombo(ctx, namespaceID, mod.ID); err != nil { + var s *automation.Script + + if _, s, err = svc.loadCombo(ctx, namespaceID, mod.ID); err != nil { return err - } else if !svc.ac.CanUpdateAutomationScript(ctx, s) { - return ErrNoCreatePermissions.withStack() - } else { - // Users need to have grant privileges to - // set script runner - if mod.RunAs != s.RunAs { - if !svc.ac.CanGrant(ctx) { - return ErrNoGrantPermissions - } - } - - s.Name = mod.Name - s.SourceRef = mod.SourceRef - s.Source = mod.Source - s.Async = mod.Async - s.RunAs = mod.RunAs - s.RunInUA = mod.RunInUA - s.Timeout = mod.Timeout - s.Critical = mod.Critical - s.Enabled = mod.Enabled - - return svc.scriptManager.UpdateScript(ctx, s) } + + if !svc.ac.CanUpdateAutomationScript(ctx, s) { + return ErrNoCreatePermissions.withStack() + } + + // Users need to have grant privileges to + // set script runner + if mod.RunAs != s.RunAs { + if !svc.ac.CanGrant(ctx) { + return ErrNoGrantPermissions + } + } + + s.Name = mod.Name + s.SourceRef = mod.SourceRef + s.Source = mod.Source + s.Async = mod.Async + s.RunAs = mod.RunAs + s.RunInUA = mod.RunInUA + s.Timeout = mod.Timeout + s.Critical = mod.Critical + s.Enabled = mod.Enabled + + err = mod.Triggers().Walk(func(t *automation.Trigger) error { + return svc.isValidTrigger(ctx, t, namespaceID) + }) + + if err != nil { + return + } + + s.AddTrigger(automation.STMS_UPDATE, mod.Triggers()...) + + return svc.scriptManager.UpdateScript(ctx, s) } func (svc automationScript) Delete(ctx context.Context, namespaceID, scriptID uint64) (err error) { @@ -155,3 +185,36 @@ func (svc automationScript) loadCombo(ctx context.Context, namespaceID, scriptID return } + +func (svc automationScript) isValidTrigger(ctx context.Context, t *automation.Trigger, namespaceID uint64) error { + if t.Resource != "compose:record" { + // Accepting only compose:record resources + return automation.ErrAutomationTriggerInvalidResource + } + + if t.IsDeferred() { + // @todo validate condition for deferred triggers + return nil + } + + switch t.Event { + case "manual", + "beforeCreate", "beforeUpdate", "beforeDelete", + "afterCreate", "afterUpdate", "afterDelete": + var moduleID = t.Uint64Condition() + + if t.Event != "manual" && moduleID == 0 { + return automation.ErrAutomationTriggerInvalidCondition + } + + if m, err := svc.mod.With(ctx).FindByID(namespaceID, moduleID); err != nil { + return err + } else if !svc.ac.CanManageAutomationTriggersOnModule(ctx, m) { + return ErrNoTriggerManagementPermissions + } + default: + return automation.ErrAutomationTriggerInvalidEvent + } + + return nil +} diff --git a/compose/internal/service/error.go b/compose/internal/service/error.go index cc64fc639..809f0b474 100644 --- a/compose/internal/service/error.go +++ b/compose/internal/service/error.go @@ -9,16 +9,17 @@ type ( ) const ( - ErrInvalidID serviceError = "InvalidID" - ErrStaleData serviceError = "StaleData" - ErrNoGrantPermissions serviceError = "NoGrantPermissions" - ErrNoCreatePermissions serviceError = "NoCreatePermissions" - ErrNoReadPermissions serviceError = "NoReadPermissions" - ErrNoUpdatePermissions serviceError = "NoUpdatePermissions" - ErrNoDeletePermissions serviceError = "NoDeletePermissions" - ErrNamespaceRequired serviceError = "NamespaceRequired" - ErrModulePageExists serviceError = "ModulePageExists" - ErrNotImplemented serviceError = "NotImplemented" + ErrInvalidID serviceError = "InvalidID" + ErrStaleData serviceError = "StaleData" + ErrNoGrantPermissions serviceError = "NoGrantPermissions" + ErrNoCreatePermissions serviceError = "NoCreatePermissions" + ErrNoReadPermissions serviceError = "NoReadPermissions" + ErrNoUpdatePermissions serviceError = "NoUpdatePermissions" + ErrNoDeletePermissions serviceError = "NoDeletePermissions" + ErrNoTriggerManagementPermissions serviceError = "NoTriggerManagementPermissions" + ErrNamespaceRequired serviceError = "NamespaceRequired" + ErrModulePageExists serviceError = "ModulePageExists" + ErrNotImplemented serviceError = "NotImplemented" ) func (e serviceError) Error() string { diff --git a/compose/internal/service/service.go b/compose/internal/service/service.go index 6ffce0834..015f85908 100644 --- a/compose/internal/service/service.go +++ b/compose/internal/service/service.go @@ -77,6 +77,8 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { permissions.Repository(db, "compose_permission_rules")) DefaultAccessControl = AccessControl(DefaultPermissions) + DefaultNamespace = Namespace() + DefaultModule = Module() { systemClientConn, err := NewSystemGRPCClient(ctx, c.GRPCClientSystem, DefaultLogger) @@ -128,10 +130,7 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { ) } - // Compose internals: - DefaultNamespace = Namespace() DefaultRecord = Record() - DefaultModule = Module() DefaultPage = Page() DefaultChart = Chart() DefaultNotification = Notification() diff --git a/compose/rest/automation_trigger.go b/compose/rest/automation_trigger.go index 76d91368c..68950c7b8 100644 --- a/compose/rest/automation_trigger.go +++ b/compose/rest/automation_trigger.go @@ -4,7 +4,6 @@ import ( "context" "github.com/pkg/errors" - "github.com/titpetric/factory/resputil" "github.com/cortezaproject/corteza-server/compose/internal/service" "github.com/cortezaproject/corteza-server/compose/rest/request" @@ -78,22 +77,25 @@ func (ctrl AutomationTrigger) List(ctx context.Context, r *request.AutomationTri } func (ctrl AutomationTrigger) Create(ctx context.Context, r *request.AutomationTriggerCreate) (interface{}, error) { - s, _, err := ctrl.loadCombo(ctx, r.NamespaceID, r.ScriptID, 0) - if err != nil { - return nil, errors.Wrap(err, "can not create trigger") - } + // @todo trigger management is currently done through automation-script endpoints + return nil, errors.New("direct trigger management disabled") - var ( - t = &automation.Trigger{ - Event: r.Event, - Resource: r.Resource, - Condition: r.Condition, - ScriptID: s.ID, - Enabled: r.Enabled, - } - ) - - return ctrl.makePayload(ctx, t, ctrl.triggers.Create(ctx, s, t)) + // s, _, err := ctrl.loadCombo(ctx, r.NamespaceID, r.ScriptID, 0) + // if err != nil { + // return nil, errors.Wrap(err, "can not create trigger") + // } + // + // var ( + // t = &automation.Trigger{ + // Event: r.Event, + // Resource: r.Resource, + // Condition: r.Condition, + // ScriptID: s.ID, + // Enabled: r.Enabled, + // } + // ) + // + // return ctrl.makePayload(ctx, t, ctrl.triggers.Create(ctx, s, t)) } func (ctrl AutomationTrigger) Read(ctx context.Context, r *request.AutomationTriggerRead) (interface{}, error) { @@ -106,27 +108,33 @@ func (ctrl AutomationTrigger) Read(ctx context.Context, r *request.AutomationTri } func (ctrl AutomationTrigger) Update(ctx context.Context, r *request.AutomationTriggerUpdate) (interface{}, error) { - s, t, err := ctrl.loadCombo(ctx, r.NamespaceID, r.ScriptID, r.TriggerID) - if err != nil { - return nil, errors.Wrap(err, "can not update trigger") - } + // @todo trigger management is currently done through automation-script endpoints + return nil, errors.New("direct trigger management disabled") - t.Event = r.Event - t.Resource = r.Resource - t.Condition = r.Condition - t.ScriptID = r.ScriptID - t.Enabled = r.Enabled - - return ctrl.makePayload(ctx, t, ctrl.triggers.Update(ctx, s, t)) + // s, t, err := ctrl.loadCombo(ctx, r.NamespaceID, r.ScriptID, r.TriggerID) + // if err != nil { + // return nil, errors.Wrap(err, "can not update trigger") + // } + // + // t.Event = r.Event + // t.Resource = r.Resource + // t.Condition = r.Condition + // t.ScriptID = r.ScriptID + // t.Enabled = r.Enabled + // + // return ctrl.makePayload(ctx, t, ctrl.triggers.Update(ctx, s, t)) } func (ctrl AutomationTrigger) Delete(ctx context.Context, r *request.AutomationTriggerDelete) (interface{}, error) { - trigger, err := ctrl.triggers.FindByID(ctx, r.TriggerID) - if err != nil { - return nil, errors.Wrap(err, "can not delete trigger") - } + // @todo trigger management is currently done through automation-script endpoints + return nil, errors.New("direct trigger management disabled") - return resputil.OK(), ctrl.triggers.Delete(ctx, trigger) + // trigger, err := ctrl.triggers.FindByID(ctx, r.TriggerID) + // if err != nil { + // return nil, errors.Wrap(err, "can not delete trigger") + // } + // + // return resputil.OK(), ctrl.triggers.Delete(ctx, trigger) } func (ctrl AutomationTrigger) loadCombo(ctx context.Context, namespaceID, scriptID, triggerID uint64) (s *automation.Script, t *automation.Trigger, err error) { diff --git a/pkg/automation/trigger.go b/pkg/automation/trigger.go index 223530dd1..3bfc410de 100644 --- a/pkg/automation/trigger.go +++ b/pkg/automation/trigger.go @@ -1,6 +1,8 @@ package automation import ( + "errors" + "strconv" "time" "github.com/cortezaproject/corteza-server/pkg/rh" @@ -66,6 +68,12 @@ const ( EVENT_TYPE_DEFERRED = "deferred" ) +var ( + ErrAutomationTriggerInvalidResource = errors.New("AutomationTriggerInvalidResource") + ErrAutomationTriggerInvalidCondition = errors.New("AutomationTriggerInvalidCondition") + ErrAutomationTriggerInvalidEvent = errors.New("AutomationTriggerInvalidEvent") +) + // IsValid checks if trigger is enabled and not deleted func (t *Trigger) IsValid() bool { return t != nil && t.Enabled && t.DeletedAt == nil @@ -81,6 +89,14 @@ func (t Trigger) IsDeferred() bool { return t.Event == EVENT_TYPE_DEFERRED } +// Uint64Condition converts condition to uint64 +// +// Errors are ignored +func (t Trigger) Uint64Condition() (o uint64) { + o, _ = strconv.ParseUint(t.Condition, 10, 64) + return +} + // HasMatch checks if any og the triggers in a set matches the given parameters func (set TriggerSet) HasMatch(m Trigger, ff ...TriggerConditionChecker) bool { withTriggers: