Support for manual/explicit running of user scripts
Moved user-script endponts under /automation/ Add permission checking for trigger running
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -2,9 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
type (
|
||||
automationRunner struct {
|
||||
ac automationRunnerAccessControler
|
||||
logger *zap.Logger
|
||||
runner proto.ScriptRunnerClient
|
||||
scriptFinder automationScriptsFinder
|
||||
@@ -25,12 +26,22 @@ type (
|
||||
|
||||
automationScriptsFinder interface {
|
||||
Watch(ctx context.Context)
|
||||
FindRunnableScripts(event, resource string, cc ...automation.TriggerConditionChecker) automation.ScriptSet
|
||||
FindRunnableScripts(resource, event string, cc ...automation.TriggerConditionChecker) automation.ScriptSet
|
||||
}
|
||||
|
||||
automationRunnerAccessControler interface {
|
||||
CanRunAutomationTrigger(ctx context.Context, r *automation.Trigger) bool
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
AutomationResourceRecord = "compose:record"
|
||||
)
|
||||
|
||||
func AutomationRunner(f automationScriptsFinder, r proto.ScriptRunnerClient) automationRunner {
|
||||
var svc = automationRunner{
|
||||
ac: DefaultAccessControl,
|
||||
|
||||
scriptFinder: f,
|
||||
runner: r,
|
||||
|
||||
@@ -41,42 +52,10 @@ func AutomationRunner(f automationScriptsFinder, r proto.ScriptRunnerClient) aut
|
||||
return svc
|
||||
}
|
||||
|
||||
func (svc automationRunner) findRecordScripts(event string, moduleID uint64) (ss automation.ScriptSet) {
|
||||
const resource = "compose:record"
|
||||
|
||||
// We'll be comparing strings, not uint64!
|
||||
var moduleIDs = strconv.FormatUint(moduleID, 10)
|
||||
|
||||
return svc.scriptFinder.FindRunnableScripts(event, resource,
|
||||
// ModuleID MUST match
|
||||
func(cModuleID string) bool {
|
||||
return moduleIDs == cModuleID
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (svc automationRunner) Watch(ctx context.Context) {
|
||||
svc.scriptFinder.Watch(ctx)
|
||||
}
|
||||
|
||||
// ManualRecordRun - Manual trigger run
|
||||
//
|
||||
// This is explicitly called, extra security check is needed
|
||||
func (svc automationRunner) ManualRecordRun(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
// @todo security check (can user run this script (scriptID) manually)
|
||||
|
||||
runner := svc.makeRecordScriptRunner(ctx, ns, m, r, true)
|
||||
|
||||
return svc.findRecordScripts("manual", m.ID).Walk(func(script *automation.Script) error {
|
||||
// Interested in a specific script, so skip everything else
|
||||
if script.ID != scriptID {
|
||||
return nil
|
||||
}
|
||||
|
||||
return runner(script)
|
||||
})
|
||||
}
|
||||
|
||||
// BeforeRecordCreate - run scripts before record is created
|
||||
//
|
||||
// This is implicitly called, no extra security check is needed
|
||||
@@ -131,6 +110,79 @@ func (svc automationRunner) AfterRecordDelete(ctx context.Context, ns *types.Nam
|
||||
)
|
||||
}
|
||||
|
||||
// Finds all scripts that are implicitly triggered by backend actions before/after
|
||||
func (svc automationRunner) findRecordScripts(event string, moduleID uint64) automation.ScriptSet {
|
||||
ss, _ := svc.scriptFinder.FindRunnableScripts(AutomationResourceRecord, event, automation.MakeMatcherIDCondition(moduleID)).
|
||||
Filter(func(script *automation.Script) (bool, error) {
|
||||
// Filter out user-agent scripts
|
||||
return !script.RunInUA, nil
|
||||
})
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// UserScripts - collect all scripts runnable by users, appends compatible triggers
|
||||
//
|
||||
// So, either in their browser (RunInUA) or by running backend scripts explicitly (event:manual)
|
||||
// All triggers are permission-checked for "run" operation.
|
||||
//
|
||||
func (svc automationRunner) UserScripts(ctx context.Context) automation.ScriptSet {
|
||||
var ss = automation.ScriptSet{}
|
||||
|
||||
_ = svc.scriptFinder.FindRunnableScripts("", "").Walk(func(script *automation.Script) error {
|
||||
var tt = []*automation.Trigger{}
|
||||
|
||||
for _, t := range script.Triggers() {
|
||||
if (script.RunInUA || t.Event == "manual") && svc.ac.CanRunAutomationTrigger(ctx, t) {
|
||||
// Making a copy so that we do not corrupt the
|
||||
tt = append(tt, &(*t))
|
||||
}
|
||||
}
|
||||
|
||||
// Have any triggers left?
|
||||
if len(tt) > 0 {
|
||||
var sc = &automation.Script{}
|
||||
|
||||
*sc = *script
|
||||
|
||||
// Replace triggers with a new set
|
||||
sc.AddTrigger(automation.STMS_REPLACE, tt...)
|
||||
|
||||
// andd append t
|
||||
ss = append(ss, sc)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
// ManualRecordRun - Manual trigger run
|
||||
//
|
||||
// This is explicitly called, extra security check is needed
|
||||
func (svc automationRunner) RecordManual(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
// This scripts are all prechecked & filtered
|
||||
script := svc.UserScripts(ctx).FindByID(scriptID)
|
||||
|
||||
if script == nil {
|
||||
return errors.New("can not find compatible script")
|
||||
}
|
||||
|
||||
// Do not execute UA scripts
|
||||
if script.RunInUA {
|
||||
return errors.New("can not execute user-agent scripts")
|
||||
}
|
||||
|
||||
// Make record script runner and
|
||||
runner := svc.makeRecordScriptRunner(ctx, ns, m, r, false)
|
||||
|
||||
// Run it with a script
|
||||
//
|
||||
// Successfully executed record scripts can have an effect on given record value (r)
|
||||
return runner(script)
|
||||
}
|
||||
|
||||
// Runs record script
|
||||
//
|
||||
// We set-up script-running environment: security (definer / invoker), async, critical
|
||||
@@ -147,6 +199,18 @@ func (svc automationRunner) makeRecordScriptRunner(ctx context.Context, ns *type
|
||||
svc.logger.Debug("executing script", zap.Any("record", r))
|
||||
|
||||
return func(script *automation.Script) error {
|
||||
if !script.IsValid() {
|
||||
return errors.New("refusing to run invalid script")
|
||||
}
|
||||
|
||||
if script.RunInUA {
|
||||
return errors.New("refusing to run user-agent script")
|
||||
}
|
||||
|
||||
if svc.runner == nil {
|
||||
return errors.New("can not run corredor script: not connected")
|
||||
}
|
||||
|
||||
// This could be executed in a goroutine (by *after triggers,
|
||||
// so we need ot rewire the sentry panic recoverty
|
||||
defer sentry.Recover()
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
service_mocks "github.com/cortezaproject/corteza-server/compose/internal/service/mocks"
|
||||
"github.com/cortezaproject/corteza-server/internal/test"
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
)
|
||||
|
||||
func Test_automationRunner_findImplicitScripts(t *testing.T) {
|
||||
var (
|
||||
// Should be a part of users-scripts, with only one (manual) trigger
|
||||
s1 = &automation.Script{ID: 1000, Enabled: true}
|
||||
s1t1 = &automation.Trigger{ID: 1001, Enabled: true, Resource: "res", Condition: "5555", Event: "manual"}
|
||||
s1t2 = &automation.Trigger{ID: 1002, Enabled: true, Resource: "res", Condition: "5555", Event: "beforeCreate"}
|
||||
s1t3 = &automation.Trigger{ID: 1003, Enabled: true, Resource: "res", Condition: "5555", Event: "afterDelete"}
|
||||
|
||||
// Should be a part of user-scripts, with all triggers
|
||||
s2 = &automation.Script{ID: 2000, Enabled: true, RunInUA: true}
|
||||
s2t1 = &automation.Trigger{ID: 2001, Enabled: true, Resource: "res", Condition: "5555", Event: "manual"}
|
||||
s2t2 = &automation.Trigger{ID: 2002, Enabled: true, Resource: "res", Condition: "5555", Event: "beforeCreate"}
|
||||
s2t3 = &automation.Trigger{ID: 2003, Enabled: true, Resource: "res", Condition: "5555", Event: "afterDelete"}
|
||||
|
||||
// Should not be a part of the user-scripts
|
||||
s3 = &automation.Script{ID: 3000, Enabled: true}
|
||||
)
|
||||
|
||||
s1.AddTrigger(automation.STMS_REPLACE, s1t1, s1t2, s1t3)
|
||||
s2.AddTrigger(automation.STMS_REPLACE, s2t1, s2t2, s2t3)
|
||||
|
||||
var runnables = automation.ScriptSet{s1, s2, s3}
|
||||
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
sfMock := service_mocks.NewMockautomationScriptsFinder(mockCtrl)
|
||||
sfMock.EXPECT().
|
||||
FindRunnableScripts(gomock.Eq(AutomationResourceRecord), gomock.Eq("beforeCreate"), gomock.Any()).
|
||||
Return(runnables)
|
||||
|
||||
runner := automationRunner{
|
||||
scriptFinder: sfMock,
|
||||
}
|
||||
|
||||
ss := runner.findRecordScripts("beforeCreate", 5555)
|
||||
|
||||
test.Assert(t, len(ss) == 2, "Received user scripts do not match")
|
||||
|
||||
test.Assert(t, len(runnables) == 3, "Expected runnable scriptSet to be intact")
|
||||
test.Assert(t, len(runnables.FindByID(1000).Triggers()) == 3, "Expected runnable scriptSet (triggers from first script) to be intact")
|
||||
}
|
||||
|
||||
func Test_automationRunner_UserScripts(t *testing.T) {
|
||||
var (
|
||||
// Should be a part of users-scripts, with only one (manual) trigger
|
||||
s1 = &automation.Script{ID: 1000, Enabled: true}
|
||||
s1t1 = &automation.Trigger{ID: 1001, Enabled: true, Event: "manual"}
|
||||
s1t2 = &automation.Trigger{ID: 1002, Enabled: true, Event: "beforeCreate"}
|
||||
s1t3 = &automation.Trigger{ID: 1003, Enabled: true, Event: "afterDelete"}
|
||||
|
||||
// Should be a part of user-scripts, with all triggers
|
||||
s2 = &automation.Script{ID: 2000, Enabled: true, RunInUA: true}
|
||||
s2t1 = &automation.Trigger{ID: 2001, Enabled: true, Event: "manual"}
|
||||
s2t2 = &automation.Trigger{ID: 2002, Enabled: true, Event: "beforeCreate"}
|
||||
s2t3 = &automation.Trigger{ID: 2003, Enabled: true, Event: "afterDelete"}
|
||||
|
||||
// Should not be a part of the user-scripts
|
||||
s3 = &automation.Script{ID: 3000, Enabled: true}
|
||||
)
|
||||
|
||||
s1.AddTrigger(automation.STMS_REPLACE, s1t1, s1t2, s1t3)
|
||||
s2.AddTrigger(automation.STMS_REPLACE, s2t1, s2t2, s2t3)
|
||||
|
||||
var runnables = automation.ScriptSet{s1, s2, s3}
|
||||
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
acAllowMock := service_mocks.NewMockautomationRunnerAccessControler(mockCtrl)
|
||||
acAllowMock.EXPECT().
|
||||
CanRunAutomationTrigger(gomock.Any(), gomock.Any()).
|
||||
// Should be equal to number of matching triggers
|
||||
Times(4).
|
||||
Return(true)
|
||||
|
||||
sfMock := service_mocks.NewMockautomationScriptsFinder(mockCtrl)
|
||||
sfMock.EXPECT().
|
||||
FindRunnableScripts(gomock.Eq(""), gomock.Eq("")).
|
||||
Return(runnables)
|
||||
|
||||
runner := automationRunner{
|
||||
ac: acAllowMock,
|
||||
scriptFinder: sfMock,
|
||||
}
|
||||
|
||||
ss := runner.UserScripts(context.Background())
|
||||
|
||||
test.Assert(t, len(ss) == 2, "Received user scripts do not match")
|
||||
test.Assert(t, len(ss.FindByID(1000).Triggers()) == 1, "Received user script triggers do not match")
|
||||
test.Assert(t, len(ss.FindByID(2000).Triggers()) == 3, "Received user script triggers do not match")
|
||||
|
||||
test.Assert(t, len(runnables) == 3, "Expected runnable scriptSet to be intact")
|
||||
test.Assert(t, len(runnables.FindByID(1000).Triggers()) == 3, "Expected runnable scriptSet (triggers from first script) to be intact")
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: compose/internal/service/automation_runner.go
|
||||
|
||||
// Package service_mocks is a generated GoMock package.
|
||||
package service_mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
automation "github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockautomationScriptsFinder is a mock of automationScriptsFinder interface
|
||||
type MockautomationScriptsFinder struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockautomationScriptsFinderMockRecorder
|
||||
}
|
||||
|
||||
// MockautomationScriptsFinderMockRecorder is the mock recorder for MockautomationScriptsFinder
|
||||
type MockautomationScriptsFinderMockRecorder struct {
|
||||
mock *MockautomationScriptsFinder
|
||||
}
|
||||
|
||||
// NewMockautomationScriptsFinder creates a new mock instance
|
||||
func NewMockautomationScriptsFinder(ctrl *gomock.Controller) *MockautomationScriptsFinder {
|
||||
mock := &MockautomationScriptsFinder{ctrl: ctrl}
|
||||
mock.recorder = &MockautomationScriptsFinderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockautomationScriptsFinder) EXPECT() *MockautomationScriptsFinderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Watch mocks base method
|
||||
func (m *MockautomationScriptsFinder) Watch(ctx context.Context) {
|
||||
m.ctrl.Call(m, "Watch", ctx)
|
||||
}
|
||||
|
||||
// Watch indicates an expected call of Watch
|
||||
func (mr *MockautomationScriptsFinderMockRecorder) Watch(ctx interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockautomationScriptsFinder)(nil).Watch), ctx)
|
||||
}
|
||||
|
||||
// FindRunnableScripts mocks base method
|
||||
func (m *MockautomationScriptsFinder) FindRunnableScripts(resource, event string, cc ...automation.TriggerConditionChecker) automation.ScriptSet {
|
||||
varargs := []interface{}{resource, event}
|
||||
for _, a := range cc {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "FindRunnableScripts", varargs...)
|
||||
ret0, _ := ret[0].(automation.ScriptSet)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// FindRunnableScripts indicates an expected call of FindRunnableScripts
|
||||
func (mr *MockautomationScriptsFinderMockRecorder) FindRunnableScripts(resource, event interface{}, cc ...interface{}) *gomock.Call {
|
||||
varargs := append([]interface{}{resource, event}, cc...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindRunnableScripts", reflect.TypeOf((*MockautomationScriptsFinder)(nil).FindRunnableScripts), varargs...)
|
||||
}
|
||||
|
||||
// MockautomationRunnerAccessControler is a mock of automationRunnerAccessControler interface
|
||||
type MockautomationRunnerAccessControler struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockautomationRunnerAccessControlerMockRecorder
|
||||
}
|
||||
|
||||
// MockautomationRunnerAccessControlerMockRecorder is the mock recorder for MockautomationRunnerAccessControler
|
||||
type MockautomationRunnerAccessControlerMockRecorder struct {
|
||||
mock *MockautomationRunnerAccessControler
|
||||
}
|
||||
|
||||
// NewMockautomationRunnerAccessControler creates a new mock instance
|
||||
func NewMockautomationRunnerAccessControler(ctrl *gomock.Controller) *MockautomationRunnerAccessControler {
|
||||
mock := &MockautomationRunnerAccessControler{ctrl: ctrl}
|
||||
mock.recorder = &MockautomationRunnerAccessControlerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockautomationRunnerAccessControler) EXPECT() *MockautomationRunnerAccessControlerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CanRunAutomationTrigger mocks base method
|
||||
func (m *MockautomationRunnerAccessControler) CanRunAutomationTrigger(ctx context.Context, r *automation.Trigger) bool {
|
||||
ret := m.ctrl.Call(m, "CanRunAutomationTrigger", ctx, r)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CanRunAutomationTrigger indicates an expected call of CanRunAutomationTrigger
|
||||
func (mr *MockautomationRunnerAccessControlerMockRecorder) CanRunAutomationTrigger(ctx, r interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanRunAutomationTrigger", reflect.TypeOf((*MockautomationRunnerAccessControler)(nil).CanRunAutomationTrigger), ctx, r)
|
||||
}
|
||||
@@ -41,7 +41,6 @@ type (
|
||||
}
|
||||
|
||||
RecordScriptsRunner interface {
|
||||
ManualRecordRun(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
BeforeRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
AfterRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
BeforeRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
@@ -63,8 +62,6 @@ type (
|
||||
Update(record *types.Record) (*types.Record, error)
|
||||
|
||||
DeleteByID(namespaceID, recordID uint64) error
|
||||
|
||||
RunScript(namespaceID, moduleID, recordID, scriptID uint64) error
|
||||
}
|
||||
|
||||
Encoder interface {
|
||||
|
||||
@@ -92,13 +92,22 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) {
|
||||
DefaultAutomationScriptManager = AutomationScript(ias)
|
||||
DefaultAutomationTriggerManager = AutomationTrigger(ias)
|
||||
|
||||
corredor, err := automation.Corredor(ctx, c.ScriptRunner, DefaultLogger)
|
||||
log.Info("initializing corredor connection", zap.String("addr", c.ScriptRunner.Addr), zap.Error(err))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
{
|
||||
var scriptRunnerClient proto.ScriptRunnerClient
|
||||
|
||||
DefaultAutomationRunner = AutomationRunner(ias, proto.NewScriptRunnerClient(corredor))
|
||||
if c.ScriptRunner.Enabled {
|
||||
corredor, err := automation.Corredor(ctx, c.ScriptRunner, DefaultLogger)
|
||||
|
||||
log.Info("initializing corredor connection", zap.String("addr", c.ScriptRunner.Addr), zap.Error(err))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scriptRunnerClient = proto.NewScriptRunnerClient(corredor)
|
||||
}
|
||||
|
||||
DefaultAutomationRunner = AutomationRunner(ias, scriptRunnerClient)
|
||||
}
|
||||
|
||||
// Compose internals:
|
||||
DefaultNamespace = Namespace()
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/internal/service"
|
||||
"github.com/cortezaproject/corteza-server/compose/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
)
|
||||
@@ -31,9 +32,27 @@ type (
|
||||
Set []*automationScriptPayload `json:"set"`
|
||||
}
|
||||
|
||||
automationScriptRunnablePayload struct {
|
||||
Set []*automationScriptRunnable `json:"set"`
|
||||
}
|
||||
|
||||
automationScriptRunnable struct {
|
||||
ScriptID uint64 `json:"scriptID,string"`
|
||||
Name string `json:"name"`
|
||||
Events map[string][]string `json:"events"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Async bool `json:"async"`
|
||||
RunInUA bool `json:"runInUA"`
|
||||
}
|
||||
|
||||
AutomationScript struct {
|
||||
scripts automationScriptService
|
||||
runner automationScriptRunner
|
||||
ac automationScriptAccessController
|
||||
|
||||
namespace automationScriptNamespaceLoader
|
||||
module automationScriptModuleLoader
|
||||
record automationScriptRecordLoader
|
||||
}
|
||||
|
||||
automationScriptService interface {
|
||||
@@ -44,18 +63,40 @@ type (
|
||||
Delete(context.Context, uint64, *automation.Script) error
|
||||
}
|
||||
|
||||
automationScriptRunner interface {
|
||||
UserScripts(context.Context) automation.ScriptSet
|
||||
RecordManual(context.Context, uint64, *types.Namespace, *types.Module, *types.Record) (err error)
|
||||
}
|
||||
|
||||
automationScriptAccessController interface {
|
||||
CanGrant(context.Context) bool
|
||||
|
||||
CanUpdateAutomationScript(context.Context, *automation.Script) bool
|
||||
CanDeleteAutomationScript(context.Context, *automation.Script) bool
|
||||
}
|
||||
|
||||
automationScriptNamespaceLoader interface {
|
||||
FindByID(uint64) (*types.Namespace, error)
|
||||
}
|
||||
|
||||
automationScriptModuleLoader interface {
|
||||
FindByID(uint64, uint64) (*types.Module, error)
|
||||
}
|
||||
|
||||
automationScriptRecordLoader interface {
|
||||
FindByID(uint64, uint64) (*types.Record, error)
|
||||
}
|
||||
)
|
||||
|
||||
func (AutomationScript) New() *AutomationScript {
|
||||
return &AutomationScript{
|
||||
scripts: service.DefaultAutomationScriptManager,
|
||||
runner: service.DefaultAutomationRunner,
|
||||
ac: service.DefaultAccessControl,
|
||||
|
||||
namespace: service.DefaultNamespace,
|
||||
module: service.DefaultModule,
|
||||
record: service.DefaultRecord,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +171,87 @@ func (ctrl AutomationScript) Delete(ctx context.Context, r *request.AutomationSc
|
||||
return resputil.OK(), ctrl.scripts.Delete(ctx, r.NamespaceID, script)
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) Runnable(ctx context.Context, r *request.AutomationScriptRunnable) (interface{}, error) {
|
||||
var (
|
||||
rval = &automationScriptRunnablePayload{
|
||||
Set: make([]*automationScriptRunnable, 0),
|
||||
}
|
||||
)
|
||||
|
||||
return rval, ctrl.runner.UserScripts(ctx).Walk(func(script *automation.Script) error {
|
||||
// @todo filter out all modules (by t.Condition) we do not have access to
|
||||
out := &automationScriptRunnable{
|
||||
ScriptID: script.ID,
|
||||
Name: script.Name,
|
||||
Events: map[string][]string{},
|
||||
Async: script.Async,
|
||||
RunInUA: script.RunInUA,
|
||||
}
|
||||
|
||||
if script.RunInUA {
|
||||
out.Source = script.Source
|
||||
}
|
||||
|
||||
_ = script.Triggers().Walk(func(t *automation.Trigger) error {
|
||||
if r.Condition != "" && r.Condition != t.Condition {
|
||||
// When not requesting explicit module and condition does not match (module id or 0)
|
||||
// ignore
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, ok := out.Events[t.Event]; ok {
|
||||
out.Events[t.Event] = append(out.Events[t.Event], t.Condition)
|
||||
} else {
|
||||
out.Events[t.Event] = []string{t.Condition}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if len(out.Events) == 0 {
|
||||
return nil
|
||||
}
|
||||
rval.Set = append(rval.Set, out)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) Run(ctx context.Context, r *request.AutomationScriptRun) (interface{}, error) {
|
||||
var (
|
||||
err error
|
||||
ns *types.Namespace
|
||||
module *types.Module
|
||||
record *types.Record
|
||||
)
|
||||
|
||||
if ns, err = ctrl.namespace.FindByID(r.NamespaceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if module, err = ctrl.module.FindByID(ns.ID, r.ModuleID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if record, err = ctrl.record.FindByID(ns.ID, r.RecordID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = ctrl.runner.RecordManual(ctx, r.ScriptID, ns, module, record); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When record was passed return it.
|
||||
if record != nil {
|
||||
// (ab)user payload maker from record controller
|
||||
// @todo find a way how to solve this more elegantly.
|
||||
return (Record{
|
||||
ac: service.DefaultAccessControl,
|
||||
}).makePayload(ctx, module, record, nil)
|
||||
}
|
||||
|
||||
return resputil.OK(), nil
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) makePayload(ctx context.Context, s *automation.Script, err error) (*automationScriptPayload, error) {
|
||||
if err != nil || s == nil {
|
||||
return nil, err
|
||||
|
||||
@@ -34,15 +34,19 @@ type AutomationScriptAPI interface {
|
||||
Read(context.Context, *request.AutomationScriptRead) (interface{}, error)
|
||||
Update(context.Context, *request.AutomationScriptUpdate) (interface{}, error)
|
||||
Delete(context.Context, *request.AutomationScriptDelete) (interface{}, error)
|
||||
Runnable(context.Context, *request.AutomationScriptRunnable) (interface{}, error)
|
||||
Run(context.Context, *request.AutomationScriptRun) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type AutomationScript struct {
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
Runnable func(http.ResponseWriter, *http.Request)
|
||||
Run func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewAutomationScript(h AutomationScriptAPI) *AutomationScript {
|
||||
@@ -147,6 +151,46 @@ func NewAutomationScript(h AutomationScriptAPI) *AutomationScript {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Runnable: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAutomationScriptRunnable()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("AutomationScript.Runnable", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Runnable(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("AutomationScript.Runnable", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("AutomationScript.Runnable", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Run: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAutomationScriptRun()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("AutomationScript.Run", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Run(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("AutomationScript.Run", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("AutomationScript.Run", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,5 +202,7 @@ func (h AutomationScript) MountRoutes(r chi.Router, middlewares ...func(http.Han
|
||||
r.Get("/namespace/{namespaceID}/automation/script/{scriptID}", h.Read)
|
||||
r.Post("/namespace/{namespaceID}/automation/script/{scriptID}", h.Update)
|
||||
r.Delete("/namespace/{namespaceID}/automation/script/{scriptID}", h.Delete)
|
||||
r.Get("/namespace/{namespaceID}/automation/script/runnable", h.Runnable)
|
||||
r.Post("/namespace/{namespaceID}/automation/script/run", h.Run)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,21 +36,19 @@ type RecordAPI interface {
|
||||
Read(context.Context, *request.RecordRead) (interface{}, error)
|
||||
Update(context.Context, *request.RecordUpdate) (interface{}, error)
|
||||
Delete(context.Context, *request.RecordDelete) (interface{}, error)
|
||||
RunScript(context.Context, *request.RecordRunScript) (interface{}, error)
|
||||
Upload(context.Context, *request.RecordUpload) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type Record struct {
|
||||
Report func(http.ResponseWriter, *http.Request)
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Export func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
RunScript func(http.ResponseWriter, *http.Request)
|
||||
Upload func(http.ResponseWriter, *http.Request)
|
||||
Report func(http.ResponseWriter, *http.Request)
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Export func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
Upload func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewRecord(h RecordAPI) *Record {
|
||||
@@ -195,26 +193,6 @@ func NewRecord(h RecordAPI) *Record {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
RunScript: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewRecordRunScript()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("Record.RunScript", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.RunScript(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("Record.RunScript", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("Record.RunScript", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Upload: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewRecordUpload()
|
||||
@@ -248,7 +226,6 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
|
||||
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Read)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Update)
|
||||
r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Delete)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/run-script", h.RunScript)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/attachment", h.Upload)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
moduleSetPayload struct {
|
||||
Filter types.ModuleFilter `json:"filter"`
|
||||
Set []*modulePayload `json:"set"`
|
||||
}
|
||||
|
||||
modulePayload struct {
|
||||
*types.Module
|
||||
|
||||
@@ -34,14 +39,8 @@ type (
|
||||
CanUpdateRecordValue bool `json:"canUpdateRecordValue"`
|
||||
}
|
||||
|
||||
moduleSetPayload struct {
|
||||
Filter types.ModuleFilter `json:"filter"`
|
||||
Set []*modulePayload `json:"set"`
|
||||
}
|
||||
|
||||
Module struct {
|
||||
module service.ModuleService
|
||||
record service.RecordService
|
||||
ac moduleAccessController
|
||||
}
|
||||
|
||||
@@ -65,7 +64,6 @@ type (
|
||||
func (Module) New() *Module {
|
||||
return &Module{
|
||||
module: service.DefaultModule,
|
||||
record: service.DefaultRecord,
|
||||
ac: service.DefaultAccessControl,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,15 +229,6 @@ func (ctrl *Record) Export(ctx context.Context, r *request.RecordExport) (interf
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctrl *Record) RunScript(ctx context.Context, r *request.RecordRunScript) (interface{}, error) {
|
||||
return resputil.OK(), ctrl.record.RunScript(
|
||||
r.NamespaceID,
|
||||
r.ModuleID,
|
||||
r.RecordID,
|
||||
r.ScriptID,
|
||||
)
|
||||
}
|
||||
|
||||
func (ctrl Record) makePayload(ctx context.Context, m *types.Module, r *types.Record, err error) (*recordPayload, error) {
|
||||
if err != nil || r == nil {
|
||||
return nil, err
|
||||
|
||||
@@ -414,3 +414,145 @@ func (r *AutomationScriptDelete) Fill(req *http.Request) (err error) {
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAutomationScriptDelete()
|
||||
|
||||
// AutomationScript runnable request parameters
|
||||
type AutomationScriptRunnable struct {
|
||||
Resource string
|
||||
Condition string
|
||||
NamespaceID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewAutomationScriptRunnable() *AutomationScriptRunnable {
|
||||
return &AutomationScriptRunnable{}
|
||||
}
|
||||
|
||||
func (r AutomationScriptRunnable) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["resource"] = r.Resource
|
||||
out["condition"] = r.Condition
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *AutomationScriptRunnable) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := get["resource"]; ok {
|
||||
r.Resource = val
|
||||
}
|
||||
if val, ok := get["condition"]; ok {
|
||||
r.Condition = val
|
||||
}
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAutomationScriptRunnable()
|
||||
|
||||
// AutomationScript run request parameters
|
||||
type AutomationScriptRun struct {
|
||||
ScriptID uint64 `json:",string"`
|
||||
Source string
|
||||
ModuleID uint64 `json:",string"`
|
||||
RecordID uint64 `json:",string"`
|
||||
Module interface{}
|
||||
Record interface{}
|
||||
NamespaceID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewAutomationScriptRun() *AutomationScriptRun {
|
||||
return &AutomationScriptRun{}
|
||||
}
|
||||
|
||||
func (r AutomationScriptRun) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["scriptID"] = r.ScriptID
|
||||
out["source"] = r.Source
|
||||
out["moduleID"] = r.ModuleID
|
||||
out["recordID"] = r.RecordID
|
||||
out["module"] = r.Module
|
||||
out["record"] = r.Record
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *AutomationScriptRun) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["scriptID"]; ok {
|
||||
r.ScriptID = parseUInt64(val)
|
||||
}
|
||||
if val, ok := post["source"]; ok {
|
||||
r.Source = val
|
||||
}
|
||||
if val, ok := post["moduleID"]; ok {
|
||||
r.ModuleID = parseUInt64(val)
|
||||
}
|
||||
if val, ok := post["recordID"]; ok {
|
||||
r.RecordID = parseUInt64(val)
|
||||
}
|
||||
if val, ok := post["module"]; ok {
|
||||
r.Module = interface{}(val)
|
||||
}
|
||||
if val, ok := post["record"]; ok {
|
||||
r.Record = interface{}(val)
|
||||
}
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAutomationScriptRun()
|
||||
|
||||
@@ -478,70 +478,6 @@ func (r *RecordDelete) Fill(req *http.Request) (err error) {
|
||||
|
||||
var _ RequestFiller = NewRecordDelete()
|
||||
|
||||
// Record runScript request parameters
|
||||
type RecordRunScript struct {
|
||||
RecordID uint64 `json:",string"`
|
||||
ScriptID uint64 `json:",string"`
|
||||
NamespaceID uint64 `json:",string"`
|
||||
ModuleID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewRecordRunScript() *RecordRunScript {
|
||||
return &RecordRunScript{}
|
||||
}
|
||||
|
||||
func (r RecordRunScript) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["recordID"] = r.RecordID
|
||||
out["scriptID"] = r.ScriptID
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
out["moduleID"] = r.ModuleID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *RecordRunScript) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["recordID"]; ok {
|
||||
r.RecordID = parseUInt64(val)
|
||||
}
|
||||
if val, ok := post["scriptID"]; ok {
|
||||
r.ScriptID = parseUInt64(val)
|
||||
}
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewRecordRunScript()
|
||||
|
||||
// Record upload request parameters
|
||||
type RecordUpload struct {
|
||||
RecordID uint64 `json:",string"`
|
||||
|
||||
Reference in New Issue
Block a user