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:
Denis Arh
2019-08-23 13:49:36 +02:00
parent 6463df9af1
commit ffdeef1da2
22 changed files with 895 additions and 298 deletions
File diff suppressed because one or more lines are too long
+98 -34
View File
@@ -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)
}
-3
View File
@@ -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 {
+15 -6
View File
@@ -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()
+122
View File
@@ -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
+51 -5
View File
@@ -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)
})
}
+8 -31
View File
@@ -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)
})
}
+5 -7
View File
@@ -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,
}
}
-9
View File
@@ -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
+142
View File
@@ -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()
-64
View File
@@ -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"`