snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,545 @@
package service
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
internalAuth "github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/spf13/cast"
"strings"
)
type (
rbacService interface {
Can(rbac.Session, string, rbac.Resource) bool
Trace(rbac.Session, string, rbac.Resource) *rbac.Trace
Grant(context.Context, ...*rbac.Rule) error
FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet)
}
accessControl struct {
actionlog actionlog.Recorder
store store.Storer
rbac rbacService
}
)
func AccessControl(s store.Storer) *accessControl {
return &accessControl{
store: s,
rbac: rbac.Global(),
actionlog: DefaultActionlog,
}
}
func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool {
return svc.rbac.Can(rbac.ContextToSession(ctx), op, res)
}
// Effective returns a list of effective permissions for all given resource
//
// This function is auto-generated
func (svc accessControl) Effective(ctx context.Context, rr ...rbac.Resource) (ee rbac.EffectiveSet) {
for _, res := range rr {
r := res.RbacResource()
for op := range rbacResourceOperations(r) {
ee.Push(r, op, svc.can(ctx, op, res))
}
}
return
}
// Evaluate returns a list of permissions evaluated for the given user/roles combo
//
// This function is auto-generated
func (svc accessControl) Trace(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee []*rbac.Trace, err error) {
// Reusing the grant permission since this is who the feature is for
if !svc.CanGrant(ctx) {
// @todo should be altered to check grant permissions PER resource
return nil, AccessControlErrNotAllowedToSetPermissions()
}
var (
resource rbac.Resource
resources []rbac.Resource
members systemTypes.RoleMemberSet
)
if len(rr) > 0 {
resources = make([]rbac.Resource, 0, len(rr))
for _, r := range rr {
if err = rbacResourceValidator(r); err != nil {
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
}
resource, err = svc.resourceLoader(ctx, r)
if err != nil {
return
}
resources = append(resources, resource)
}
} else {
resources = svc.Resources()
}
// User ID specified, load its roles
if userID != 0 {
if len(roles) > 0 {
// should be prevented on the client
return nil, fmt.Errorf("userID and roles are mutually exclusive")
}
members, _, err = store.SearchRoleMembers(ctx, svc.store, systemTypes.RoleMemberFilter{Resource: fmt.Sprintf("corteza::system:user/%d", userID)})
if err != nil {
return nil, err
}
for _, m := range members {
roles = append(roles, m.RoleID)
}
for _, r := range internalAuth.AuthenticatedRoles() {
roles = append(roles, r.ID)
}
}
if len(roles) == 0 {
// should be prevented on the client
return nil, fmt.Errorf("no roles specified")
}
session := rbac.ParamsToSession(ctx, userID, roles...)
for _, res := range resources {
r := res.RbacResource()
for op := range rbacResourceOperations(r) {
ee = append(ee, svc.rbac.Trace(session, op, res))
}
}
return
}
// Resources returns list of resources
//
// This function is auto-generated
func (svc accessControl) Resources() []rbac.Resource {
return []rbac.Resource{
rbac.NewResource(types.WorkflowRbacResource(0)),
rbac.NewResource(types.ComponentRbacResource()),
}
}
// List returns list of operations on all resources
//
// This function is auto-generated
func (svc accessControl) List() (out []map[string]string) {
def := []map[string]string{
{
"type": types.WorkflowResourceType,
"any": types.WorkflowRbacResource(0),
"op": "read",
},
{
"type": types.WorkflowResourceType,
"any": types.WorkflowRbacResource(0),
"op": "update",
},
{
"type": types.WorkflowResourceType,
"any": types.WorkflowRbacResource(0),
"op": "delete",
},
{
"type": types.WorkflowResourceType,
"any": types.WorkflowRbacResource(0),
"op": "undelete",
},
{
"type": types.WorkflowResourceType,
"any": types.WorkflowRbacResource(0),
"op": "execute",
},
{
"type": types.WorkflowResourceType,
"any": types.WorkflowRbacResource(0),
"op": "triggers.manage",
},
{
"type": types.WorkflowResourceType,
"any": types.WorkflowRbacResource(0),
"op": "sessions.manage",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "grant",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "workflow.create",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "triggers.search",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "sessions.search",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "workflows.search",
},
{
"type": types.ComponentResourceType,
"any": types.ComponentRbacResource(),
"op": "resource-translations.manage",
},
}
func(svc interface{}) {
if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is {
def = append(def, svc.list()...)
}
}(svc)
return def
}
// Grant applies one or more RBAC rules
//
// This function is auto-generated
func (svc accessControl) Grant(ctx context.Context, rr ...*rbac.Rule) error {
if !svc.CanGrant(ctx) {
// @todo should be altered to check grant permissions PER resource
return AccessControlErrNotAllowedToSetPermissions()
}
for _, r := range rr {
err := rbacResourceValidator(r.Resource, r.Operation)
if err != nil {
return err
}
}
if err := svc.rbac.Grant(ctx, rr...); err != nil {
return AccessControlErrGeneric().Wrap(err)
}
svc.logGrants(ctx, rr)
return nil
}
// This function is auto-generated
func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) {
if svc.actionlog == nil {
return
}
for _, r := range rr {
g := AccessControlActionGrant(&accessControlActionProps{r})
g.log = r.String()
g.resource = r.Resource
svc.actionlog.Record(ctx, g.ToAction())
}
}
// FindRules find all rules based on filters
//
// This function is auto-generated
func (svc accessControl) FindRules(ctx context.Context, roleID uint64, rr ...string) (out rbac.RuleSet, err error) {
if !svc.CanGrant(ctx) {
return nil, AccessControlErrNotAllowedToSetPermissions()
}
out, err = svc.FindRulesByRoleID(ctx, roleID)
if err != nil {
return
}
var resources []rbac.Resource
if len(rr) > 0 {
resources = make([]rbac.Resource, 0, len(rr))
for _, r := range rr {
if err = rbacResourceValidator(r); err != nil {
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
}
resources = append(resources, rbac.NewResource(r))
}
} else {
resources = svc.Resources()
}
return out.FilterResource(resources...), nil
}
// FindRulesByRoleID find all rules for a specific role
//
// This function is auto-generated
func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) (rbac.RuleSet, error) {
if !svc.CanGrant(ctx) {
return nil, AccessControlErrNotAllowedToSetPermissions()
}
return svc.rbac.FindRulesByRoleID(roleID), nil
}
// CanReadWorkflow checks if current user can read workflow
//
// This function is auto-generated
func (svc accessControl) CanReadWorkflow(ctx context.Context, r *types.Workflow) bool {
return svc.can(ctx, "read", r)
}
// CanUpdateWorkflow checks if current user can update workflow
//
// This function is auto-generated
func (svc accessControl) CanUpdateWorkflow(ctx context.Context, r *types.Workflow) bool {
return svc.can(ctx, "update", r)
}
// CanDeleteWorkflow checks if current user can delete workflow
//
// This function is auto-generated
func (svc accessControl) CanDeleteWorkflow(ctx context.Context, r *types.Workflow) bool {
return svc.can(ctx, "delete", r)
}
// CanUndeleteWorkflow checks if current user can undelete workflow
//
// This function is auto-generated
func (svc accessControl) CanUndeleteWorkflow(ctx context.Context, r *types.Workflow) bool {
return svc.can(ctx, "undelete", r)
}
// CanExecuteWorkflow checks if current user can execute workflow
//
// This function is auto-generated
func (svc accessControl) CanExecuteWorkflow(ctx context.Context, r *types.Workflow) bool {
return svc.can(ctx, "execute", r)
}
// CanManageTriggersOnWorkflow checks if current user can manage workflow triggers
//
// This function is auto-generated
func (svc accessControl) CanManageTriggersOnWorkflow(ctx context.Context, r *types.Workflow) bool {
return svc.can(ctx, "triggers.manage", r)
}
// CanManageSessionsOnWorkflow checks if current user can manage workflow sessions
//
// This function is auto-generated
func (svc accessControl) CanManageSessionsOnWorkflow(ctx context.Context, r *types.Workflow) bool {
return svc.can(ctx, "sessions.manage", r)
}
// CanGrant checks if current user can manage automation permissions
//
// This function is auto-generated
func (svc accessControl) CanGrant(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "grant", r)
}
// CanCreateWorkflow checks if current user can create workflows
//
// This function is auto-generated
func (svc accessControl) CanCreateWorkflow(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "workflow.create", r)
}
// CanSearchTriggers checks if current user can list, search or filter triggers
//
// This function is auto-generated
func (svc accessControl) CanSearchTriggers(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "triggers.search", r)
}
// CanSearchSessions checks if current user can list, search or filter sessions
//
// This function is auto-generated
func (svc accessControl) CanSearchSessions(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "sessions.search", r)
}
// CanSearchWorkflows checks if current user can list, search or filter workflows
//
// This function is auto-generated
func (svc accessControl) CanSearchWorkflows(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "workflows.search", r)
}
// CanManageResourceTranslations checks if current user can list, search, create, or update resource translations
//
// This function is auto-generated
func (svc accessControl) CanManageResourceTranslations(ctx context.Context) bool {
r := &types.Component{}
return svc.can(ctx, "resource-translations.manage", r)
}
// rbacResourceValidator validates known component's resource by routing it to the appropriate validator
//
// This function is auto-generated
func rbacResourceValidator(r string, oo ...string) error {
switch rbac.ResourceType(r) {
case types.WorkflowResourceType:
return rbacWorkflowResourceValidator(r, oo...)
case types.ComponentResourceType:
return rbacComponentResourceValidator(r, oo...)
}
return fmt.Errorf("unknown resource type %q", r)
}
// resourceLoader loads resource from store
//
// # Notes
// Function assumes existence of loader functions for all resource types
//
// This function is auto-generated
func (svc accessControl) resourceLoader(ctx context.Context, resource string) (rbac.Resource, error) {
var (
hasWildcard = false
resourceType, ids = rbac.ParseResourceID(resource)
)
for _, id := range ids {
if id == 0 {
hasWildcard = true
break
}
}
switch rbac.ResourceType(resourceType) {
case types.WorkflowResourceType:
if hasWildcard {
return rbac.NewResource(types.WorkflowRbacResource(ids[0])), nil
}
return loadWorkflow(ctx, svc.store, ids[0])
case types.ComponentResourceType:
return &types.Component{}, nil
}
_ = ids
return nil, fmt.Errorf("unknown resource type %q", resourceType)
}
// rbacResourceOperations returns defined operations for a requested resource
//
// This function is auto-generated
func rbacResourceOperations(r string) map[string]bool {
switch rbac.ResourceType(r) {
case types.WorkflowResourceType:
return map[string]bool{
"read": true,
"update": true,
"delete": true,
"undelete": true,
"execute": true,
"triggers.manage": true,
"sessions.manage": true,
}
case types.ComponentResourceType:
return map[string]bool{
"grant": true,
"workflow.create": true,
"triggers.search": true,
"sessions.search": true,
"workflows.search": true,
"resource-translations.manage": true,
}
}
return nil
}
// rbacWorkflowResourceValidator checks validity of RBAC resource and operations
//
// # Notes
// Can be called without operations to check for validity of resource string only
//
// This function is auto-generated
func rbacWorkflowResourceValidator(r string, oo ...string) error {
if !strings.HasPrefix(r, types.WorkflowResourceType) {
// expecting resource to always include path
return fmt.Errorf("invalid resource type")
}
defOps := rbacResourceOperations(r)
for _, o := range oo {
if !defOps[o] {
return fmt.Errorf("invalid operation '%s' for workflow resource", o)
}
}
const sep = "/"
var (
pp = strings.Split(strings.Trim(r[len(types.WorkflowResourceType):], sep), sep)
prc = []string{
"ID",
}
)
if len(pp) != len(prc) {
return fmt.Errorf("invalid resource path structure")
}
for i := 0; i < len(pp); i++ {
if pp[i] != "*" {
if i > 0 && pp[i-1] == "*" {
return fmt.Errorf("invalid path wildcard level (%d) for workflow resource", i)
}
if _, err := cast.ToUint64E(pp[i]); err != nil {
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
}
}
}
return nil
}
// rbacComponentResourceValidator checks validity of RBAC resource and operations
//
// # Notes
// Can be called without operations to check for validity of resource string only
//
// This function is auto-generated
func rbacComponentResourceValidator(r string, oo ...string) error {
if r != types.ComponentResourceType+"/" {
// expecting resource to always include path
return fmt.Errorf("invalid component resource, expecting " + types.ComponentResourceType + "/")
}
defOps := rbacResourceOperations(r)
for _, o := range oo {
if !defOps[o] {
return fmt.Errorf("invalid operation '%s' for automation component resource", o)
}
}
return nil
}

View File

@@ -0,0 +1,284 @@
package service
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// automation/service/access_control_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"strings"
"time"
)
type (
accessControlActionProps struct {
rule *rbac.Rule
}
accessControlAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *accessControlActionProps
}
accessControlLogMetaKey struct{}
accessControlPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setRule updates accessControlActionProps's rule
//
// This function is auto-generated.
func (p *accessControlActionProps) setRule(rule *rbac.Rule) *accessControlActionProps {
p.rule = rule
return p
}
// Serialize converts accessControlActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p accessControlActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.rule != nil {
m.Set("rule.operation", p.rule.Operation, true)
m.Set("rule.roleID", p.rule.RoleID, true)
m.Set("rule.access", p.rule.Access, true)
m.Set("rule.resource", p.rule.Resource, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p accessControlActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.rule != nil {
// replacement for "{{rule}}" (in order how fields are defined)
pairs = append(
pairs,
"{{rule}}",
fns(
p.rule.Operation,
p.rule.RoleID,
p.rule.Access,
p.rule.Resource,
),
)
pairs = append(pairs, "{{rule.operation}}", fns(p.rule.Operation))
pairs = append(pairs, "{{rule.roleID}}", fns(p.rule.RoleID))
pairs = append(pairs, "{{rule.access}}", fns(p.rule.Access))
pairs = append(pairs, "{{rule.resource}}", fns(p.rule.Resource))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *accessControlAction) String() string {
var props = &accessControlActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *accessControlAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// AccessControlActionGrant returns "automation:access_control.grant" action
//
// This function is auto-generated.
func AccessControlActionGrant(props ...*accessControlActionProps) *accessControlAction {
a := &accessControlAction{
timestamp: time.Now(),
resource: "automation:access_control",
action: "grant",
log: "grant",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// AccessControlErrGeneric returns "automation:access_control.generic" as *errors.Error
//
// This function is auto-generated.
func AccessControlErrGeneric(mm ...*accessControlActionProps) *errors.Error {
var p = &accessControlActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "automation:access_control"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(accessControlLogMetaKey{}, "{err}"),
errors.Meta(accessControlPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AccessControlErrNotAllowedToSetPermissions returns "automation:access_control.notAllowedToSetPermissions" as *errors.Error
//
// This function is auto-generated.
func AccessControlErrNotAllowedToSetPermissions(mm ...*accessControlActionProps) *errors.Error {
var p = &accessControlActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to set permissions", nil),
errors.Meta("type", "notAllowedToSetPermissions"),
errors.Meta("resource", "automation:access_control"),
errors.Meta(accessControlPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "access-control.errors.notAllowedToSetPermissions"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc accessControl) recordAction(ctx context.Context, props *accessControlActionProps, actionFn func(...*accessControlActionProps) *accessControlAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(accessControlLogMetaKey{}), err)
if p, has := m[accessControlPropsMetaKey{}]; has {
a.Meta = p.(*accessControlActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,25 @@
# List of security/audit events and errors that we need to log
resource: automation:access_control
service: accessControl
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: alert
import:
- github.com/cortezaproject/corteza/server/pkg/rbac
props:
- name: rule
type: "*rbac.Rule"
fields: [ operation, roleID, access, resource ]
actions:
- action: grant
errors:
- error: notAllowedToSetPermissions
message: "not allowed to set permissions"

View File

@@ -0,0 +1,106 @@
package service
import (
"sort"
"sync"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
)
type (
registry struct {
lock *sync.RWMutex
functions map[string]*types.Function
types map[string]expr.Type
}
handler interface {
Functions() []*types.Function
}
)
var (
defaultRegistry = initRegistry()
)
func Registry() *registry {
return defaultRegistry
}
func initRegistry() *registry {
return &registry{
lock: &sync.RWMutex{},
functions: make(map[string]*types.Function),
types: make(map[string]expr.Type),
}
}
func (r *registry) AddFunctions(ff ...*types.Function) {
defer r.lock.Unlock()
r.lock.Lock()
for _, fn := range ff {
r.functions[fn.Ref] = fn
}
}
func (r *registry) AddHandlers(hh ...handler) {
for _, h := range hh {
r.AddFunctions(h.Functions()...)
}
}
func (r registry) Function(ref string) *types.Function {
defer r.lock.RUnlock()
r.lock.RLock()
return r.functions[ref]
}
func (r registry) Functions() []*types.Function {
var (
rr = make([]string, 0, len(r.functions))
ff = make([]*types.Function, 0, len(r.functions))
)
for ref, f := range r.functions {
// flag for UI weather this function step is disabled or not
f.Disabled = !DefaultWorkflow.corredorOpt.Enabled && ref == "corredorExec"
rr = append(rr, ref)
}
sort.Strings(rr)
for _, ref := range rr {
ff = append(ff, r.functions[ref])
}
return ff
}
func (r *registry) AddTypes(tt ...expr.Type) {
defer r.lock.Unlock()
r.lock.Lock()
for _, t := range tt {
r.types[t.Type()] = t
}
}
func (r *registry) Type(ref string) expr.Type {
defer r.lock.RUnlock()
r.lock.RLock()
return r.types[ref]
}
func (r *registry) Types() []string {
var (
rr = make([]string, 0, len(r.types))
)
for ref := range r.types {
rr = append(rr, ref)
}
sort.Strings(rr)
return rr
}

View File

@@ -0,0 +1,165 @@
package service
import (
"context"
"time"
"github.com/cortezaproject/corteza/server/automation/automation"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/corredor"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/objstore"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/store"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
"go.uber.org/zap"
)
type (
websocketSender interface {
Send(kind string, payload interface{}, userIDs ...uint64) error
}
Config struct {
ActionLog options.ActionLogOpt
Workflow options.WorkflowOpt
Corredor options.CorredorOpt
}
userService interface {
FindByAny(ctx context.Context, identifier interface{}) (*sysTypes.User, error)
}
)
var (
DefaultObjectStore objstore.Store
// DefaultStore is an interface to storage backend(s)
// ng (next-gen) is a temporary prefix
// so that we can differentiate between it and the file-only store
DefaultStore store.Storer
DefaultLogger *zap.Logger
// DefaultAccessControl Access control checking
DefaultAccessControl *accessControl
DefaultActionlog actionlog.Recorder
DefaultUser userService
DefaultWorkflow *workflow
DefaultTrigger *trigger
DefaultSession *session
// wrapper around time.Now() that will aid service testing
now = func() *time.Time {
c := time.Now().Round(time.Second)
return &c
}
// wrapper around nextID that will aid service testing
nextID = func() uint64 {
return id.Next()
}
)
func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, ws websocketSender, c Config) (err error) {
var (
//hcd = healthcheck.Defaults()
)
// we're doing conversion to avoid having
// store interface exposed or generated inside app package
DefaultStore = s
DefaultLogger = log.Named("workflow")
{
tee := zap.NewNop()
policy := actionlog.MakeProductionPolicy()
if !c.ActionLog.Enabled {
policy = actionlog.MakeDisabledPolicy()
} else if c.ActionLog.Debug {
policy = actionlog.MakeDebugPolicy()
tee = log
}
DefaultActionlog = actionlog.NewService(DefaultStore, log, tee, policy)
}
DefaultAccessControl = AccessControl(s)
DefaultSession = Session(DefaultLogger.Named("session"), c.Workflow, ws)
DefaultWorkflow = Workflow(DefaultLogger.Named("workflow"), c.Corredor, c.Workflow)
DefaultTrigger = Trigger(DefaultLogger.Named("trigger"), c.Workflow)
DefaultWorkflow.triggers = DefaultTrigger
Registry().AddTypes(
&expr.Any{},
&expr.Array{},
&expr.Boolean{},
&expr.ID{},
&expr.Integer{},
&expr.UnsignedInteger{},
&expr.Float{},
&expr.String{},
&expr.Handle{},
&expr.DateTime{},
&expr.Duration{},
&expr.KV{},
&expr.KVV{},
&expr.Reader{},
&expr.Vars{},
&expr.HttpRequest{},
&expr.Bytes{},
&automation.EmailMessage{},
)
automation.HttpRequestHandler(Registry())
automation.LogHandler(Registry())
automation.QueueHandler(Registry())
automation.JsenvHandler(Registry())
automation.Oauth2Handler(Registry())
automation.LoopHandler(Registry(), DefaultWorkflow.parser)
automation.CorredorHandler(Registry(), corredor.Service())
automation.EmailHandler(Registry())
automation.JwtHandler(Registry())
automation.ApigwBodyHandler(Registry())
return
}
func Activate(ctx context.Context) (err error) {
if err = DefaultWorkflow.Load(ctx); err != nil {
return
}
return
}
func Watchers(ctx context.Context) {
DefaultSession.Watch(ctx)
return
}
// Data is stale when new date does not match updatedAt or createdAt (before first update)
func isStale(new *time.Time, updatedAt *time.Time, createdAt time.Time) bool {
if new == nil {
// Change to true for stale-data-check
return false
}
if updatedAt != nil {
return !new.Equal(*updatedAt)
}
return new.Equal(createdAt)
}
// trim1st removes 1st param and returns only error
func trim1st(_ interface{}, err error) error {
return err
}

View File

@@ -0,0 +1,588 @@
package service
import (
"context"
"fmt"
"sync"
"time"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/sentry"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/store"
"github.com/modern-go/reflect2"
"go.uber.org/zap"
)
type (
promptSender interface {
Send(kind string, payload interface{}, userIDs ...uint64) error
}
session struct {
store store.Storer
actionlog actionlog.Recorder
ac sessionAccessController
opt options.WorkflowOpt
log *zap.Logger
mux sync.RWMutex
pool map[uint64]*types.Session
spawnQueue chan *spawn
promptSender promptSender
}
spawn struct {
workflowID uint64
session chan *wfexec.Session
graph *wfexec.Graph
invoker auth.Identifiable
runner auth.Identifiable
trace bool
callStack []uint64
}
sessionAccessController interface {
CanSearchSessions(context.Context) bool
CanManageSessionsOnWorkflow(context.Context, *types.Workflow) bool
}
WaitFn func(ctx context.Context) (*expr.Vars, uint64, wfexec.SessionStatus, types.Stacktrace, error)
)
const (
// when the state changes, state-change-handler is called and for non-fatal,
// non-interactive or non-delay steps (that are much more frequent) we need
// to limit how often the store is updated with the updated session info
//
// We use the size of the stacktrace and for every F (see the value of the constant)
// we flush the session info to the store.
sessionStateFlushFrequency = 1000
)
func Session(log *zap.Logger, opt options.WorkflowOpt, ps promptSender) *session {
return &session{
log: log,
opt: opt,
actionlog: DefaultActionlog,
store: DefaultStore,
ac: DefaultAccessControl,
pool: make(map[uint64]*types.Session),
spawnQueue: make(chan *spawn),
promptSender: ps,
}
}
func (svc *session) Search(ctx context.Context, filter types.SessionFilter) (rr types.SessionSet, f types.SessionFilter, err error) {
var (
sap = &sessionActionProps{filter: &filter}
)
err = func() (err error) {
if !svc.ac.CanSearchSessions(ctx) {
return SessionErrNotAllowedToSearch()
}
if rr, f, err = store.SearchAutomationSessions(ctx, svc.store, filter); err != nil {
return err
}
return nil
}()
return rr, f, svc.recordAction(ctx, sap, SessionActionSearch, err)
}
func (svc *session) LookupByID(ctx context.Context, sessionID uint64) (res *types.Session, err error) {
var (
sap = &sessionActionProps{session: &types.Session{ID: sessionID}}
wf *types.Workflow
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
if !svc.ac.CanSearchSessions(ctx) {
return SessionErrNotAllowedToRead()
}
if res, err = loadSession(ctx, s, sessionID); err != nil {
return err
}
if wf, err = loadWorkflow(ctx, s, res.WorkflowID); err != nil {
return err
}
if !svc.ac.CanManageSessionsOnWorkflow(ctx, wf) {
return SessionErrNotAllowedToManage()
}
return nil
})
return res, svc.recordAction(ctx, sap, SessionActionLookup, err)
}
func (svc *session) resumeAll(ctx context.Context) error {
// In theory we could resume active/pending/prompt sessions from persistent store
// so that they can survive server termination
// @todo resume active sessions from storage
// load all active sessions from store and load them into the pool
//
return nil
}
func (svc *session) suspendAll(ctx context.Context) error {
// In theory we could suspend active/pending/prompt sessions to persistent store
// so that they can survive server termination
// @todo suspend active sessions to storage:
// stop watcher queue
// run gc
// stop worker on each session
// flush session to store (like we're doing in the status handler
return nil
}
// PendingPrompts returns all prompts on all sessions owned by current user
func (svc *session) PendingPrompts(ctx context.Context) (pp []*wfexec.PendingPrompt) {
var (
i = auth.GetIdentityFromContext(ctx)
)
if i == nil {
return
}
svc.mux.RLock()
defer svc.mux.RUnlock()
pp = make([]*wfexec.PendingPrompt, 0, len(svc.pool))
for _, s := range svc.pool {
pp = append(pp, s.PendingPrompts(i.Identity())...)
}
return
}
// Start new workflow session on a specific step with a given identity and scope
//
// # Start is an asynchronous operation
//
// Please note that context passed to the function is NOT the the one that is
// used for the execution of the workflow. See watch function!
//
// It does not check user's permissions to execute workflow(s) so it should be used only when !
func (svc *session) Start(ctx context.Context, g *wfexec.Graph, ssp types.SessionStartParams) (wait WaitFn, _ uint64, err error) {
var (
start wfexec.Step
)
if g == nil {
return nil, 0, errors.InvalidData("cannot start workflow, uninitialized graph")
}
if len(ssp.CallStack) > svc.opt.CallStackSize {
return nil, 0, WorkflowErrMaximumCallStackSizeExceeded()
}
ssp.CallStack = append(ssp.CallStack, ssp.WorkflowID)
if ssp.Invoker == nil {
return nil, 0, errors.InvalidData("cannot start workflow without user")
}
if ssp.Runner == nil {
ssp.Runner = ssp.Invoker
}
if ssp.StepID == 0 {
// starting step is not explicitly set
// find orphan step
switch oo := g.Orphans(); len(oo) {
case 1:
start = oo[0]
case 0:
return nil, 0, errors.InvalidData("could not find starting step")
default:
return nil, 0, errors.InvalidData("cannot start workflow session multiple starting steps found")
}
} else if start = g.StepByID(ssp.StepID); start == nil {
return nil, 0, errors.InvalidData("trigger staring step references non-existing step")
} else if len(g.Parents(g.StepByID(ssp.StepID))) > 0 {
return nil, 0, errors.InvalidData("cannot start workflow on a step with parents")
}
var (
ses = svc.spawn(g, ssp.WorkflowID, ssp.Trace, ssp.CallStack, ssp.Runner, ssp.Invoker)
)
ses.CreatedAt = *now()
ses.CreatedBy = ssp.Invoker.Identity()
ses.Status = types.SessionStarted
ses.Apply(ssp)
_ = ssp.Input.AssignFieldValue("eventType", expr.Must(expr.NewString(ssp.EventType)))
_ = ssp.Input.AssignFieldValue("resourceType", expr.Must(expr.NewString(ssp.ResourceType)))
_ = ssp.Input.AssignFieldValue("invoker", expr.Must(expr.NewAny(ssp.Invoker)))
_ = ssp.Input.AssignFieldValue("runner", expr.Must(expr.NewAny(ssp.Runner)))
if err = ses.Exec(ctx, start, ssp.Input); err != nil {
return
}
return func(ctx context.Context) (scope *expr.Vars, sessionID uint64, status wfexec.SessionStatus, stacktrace types.Stacktrace, err error) {
sessionID = ses.ID
scope, status, stacktrace, err = ses.WaitResults(ctx)
return
}, ses.ID, nil
}
// Resume resumes suspended session/state
//
// Session can only be resumed by knowing session and state ID. Resume is an asynchronous operation
//
// There is minimum access-control deep inside wfexec.Session.Resume function
// that compares identity with state owner
func (svc *session) Resume(sessionID, stateID uint64, i auth.Identifiable, input *expr.Vars) error {
var (
ctx = auth.SetIdentityToContext(context.Background(), i)
)
svc.mux.RLock()
defer svc.mux.RUnlock()
ses := svc.pool[sessionID]
if ses == nil {
return errors.NotFound("session not found")
}
resPrompt, err := ses.Resume(ctx, stateID, input)
if err != nil {
return err
}
if err = svc.promptSender.Send("workflowSessionResumed", resPrompt, resPrompt.OwnerId); err != nil {
svc.log.Error("failed to send prompt resume status to user", zap.Error(err))
}
return nil
}
// Terminates session ID
func (svc *session) Cancel(ctx context.Context, sessionID uint64) (err error) {
svc.mux.RLock()
var (
wf *types.Workflow
ses = svc.pool[sessionID]
)
// unlock right away!
// when session is canceled, handler pick it up and
// locks it again
svc.mux.RUnlock()
if ses == nil {
return errors.NotFound("session not found or already canceled")
}
if wf, err = loadWorkflow(ctx, svc.store, ses.WorkflowID); err != nil {
return
}
if !svc.ac.CanManageSessionsOnWorkflow(ctx, wf) {
return SessionErrNotAllowedToManage()
}
ses.Cancel()
return nil
}
// spawns a new session
//
// We need initial context for the session because we want to catch all cancellations or timeouts from there
// and not from any potential HTTP requests or similar temporary context that can prematurely destroy a workflow session
func (svc *session) spawn(g *wfexec.Graph, workflowID uint64, trace bool, callStack []uint64, runner, invoker auth.Identifiable) (ses *types.Session) {
s := &spawn{
workflowID: workflowID,
session: make(chan *wfexec.Session, 1),
graph: g,
trace: trace,
callStack: callStack,
invoker: invoker,
runner: runner,
}
// Send new-session request
svc.spawnQueue <- s
// blocks until session is set
ses = types.NewSession(<-s.session)
if !svc.opt.StackTraceEnabled {
ses.DisableStacktrace()
}
if svc.opt.StackTraceFull {
ses.FullStacktrace()
}
if trace {
ses.Traced()
}
svc.mux.Lock()
svc.pool[ses.ID] = ses
svc.mux.Unlock()
return ses
}
// Watch looks over session's spawn queue
func (svc *session) Watch(ctx context.Context) {
gcTicker := time.NewTicker(time.Second)
lpTicker := time.NewTicker(time.Second * 30)
go func() {
defer sentry.Recover()
defer gcTicker.Stop()
defer svc.log.Info("stopped")
for {
select {
case <-ctx.Done():
return
case s := <-svc.spawnQueue:
var execCtx = context.Background()
opts := []wfexec.SessionOpt{
wfexec.SetWorkflowID(s.workflowID),
wfexec.SetCallStack(s.callStack...),
wfexec.SetHandler(svc.stateChangeHandler(ctx)),
}
if svc.opt.ExecDebug {
log := svc.log.
Named("exec").
With(logger.Uint64("workflowID", s.workflowID)).
With(logger.Uint64("runnerID", s.runner.Identity())).
With(logger.Uint64s("runnerRoles", s.runner.Roles()))
opts = append(
opts,
wfexec.SetLogger(log),
wfexec.SetDumpStacktraceOnPanic(true),
)
}
// Encode runner into execution context
// runner is used as identity and for access control
execCtx = auth.SetIdentityToContext(execCtx, s.runner)
// Encode invoker into execution context
// invoker is used
execCtx = context.WithValue(execCtx, workflowInvokerCtxKey{}, s.invoker)
s.session <- wfexec.NewSession(execCtx, s.graph, opts...)
// case time for a pool cleanup
// @todo cleanup pool when sessions are complete
case <-gcTicker.C:
svc.gc()
case <-lpTicker.C:
svc.logPending()
}
}
// @todo serialize sessions & suspended states
//svc.suspendAll(ctx)
}()
svc.log.Debug("watcher initialized")
}
// garbage collection for stale sessions
func (svc *session) gc() {
svc.mux.Lock()
defer svc.mux.Unlock()
for _, s := range svc.pool {
if s.GC() {
delete(svc.pool, s.ID)
}
}
}
// garbage collection for stale sessions
func (svc *session) logPending() {
svc.mux.RLock()
defer svc.mux.RUnlock()
var (
total = len(svc.pool)
pending, pending1m, pending1h, pending1d int
)
for _, s := range svc.pool {
switch age := now().Sub(s.CreatedAt); {
case age > time.Hour*24:
pending1d++
case age > time.Hour:
pending1h++
case age > time.Minute:
pending1m++
default:
pending++
}
}
if total > 0 {
svc.log.Info(
"workflow session garbage collector stats",
zap.Int("total", total),
zap.Int("pending", pending),
zap.Int("pending1m", pending1m),
zap.Int("pending1h", pending1h),
zap.Int("pending1d", pending1d),
)
}
}
// stateChangeHandler keeps track of session status changes and frequently stores session into db
func (svc *session) stateChangeHandler(ctx context.Context) wfexec.StateChangeHandler {
return func(status wfexec.SessionStatus, state *wfexec.State, s *wfexec.Session) (err error) {
svc.mux.Lock()
defer svc.mux.Unlock()
// Handle potential state change handler panics and gracefully error out
defer func() {
reason := recover()
if reflect2.IsNil(reason) {
return
}
err = fmt.Errorf("automation/service/session.go#stateChangeHandler panic: %v", reason)
}()
ses := svc.pool[s.ID()]
if ses == nil {
log := svc.log.With(
logger.Uint64("sessionID", s.ID()),
zap.Stringer("status", status),
)
log.Warn("could not find session to update")
return
}
ses.FlushCounter++
var (
// By default, we want to update session when new status is prompted, delayed, completed, canceled or failed
// But if status is active, we'll flush it every X frames (sessionStateFlushFrequency)
update = true
frame *wfexec.Frame
)
// Building a frame deep-copies the whole scope, so only do it when the
// variables are going to be read back: when the session is traced, or
// when it is settling on a status someone will want explained.
//
// ElapsedTime is calculated on append; the oldest frames can be
// dropped, so the session keeps track of when tracing started.
if state != nil && svc.opt.StackTraceEnabled {
if ses.KeepsFrameScope() || status != wfexec.SessionActive {
frame = state.MakeFrame()
} else {
frame = state.MakeLightFrame()
}
ses.AppendRuntimeStacktrace(frame)
}
switch status {
case wfexec.SessionPrompted:
ses.SuspendedAt = now()
ses.Status = types.SessionPrompted
// Send the pending prompts to user
if svc.promptSender != nil {
for _, pp := range s.UnsentPendingPrompts() {
// Prevent prompts from being re-sent
//
// This here collects all of the pending prompts and (re-)sends them.
// The problem occurs if you display multiple prompts in parallel --
// they get duplicated.
//
// Patching back-end with this simple fix, but we should probably
// refactor the logic that handles prompting a bit.
pp.Original.MarkSent()
if err := svc.promptSender.Send("workflowSessionPrompt", pp, pp.OwnerId); err != nil {
svc.log.Error("failed to send prompt to user", zap.Error(err))
}
}
}
case wfexec.SessionDelayed:
ses.SuspendedAt = now()
ses.Status = types.SessionSuspended
case wfexec.SessionCompleted:
ses.SuspendedAt = nil
ses.CompletedAt = now()
ses.Status = types.SessionCompleted
case wfexec.SessionFailed:
ses.SuspendedAt = nil
ses.CompletedAt = now()
if state != nil {
ses.Error = state.Error()
}
ses.Status = types.SessionFailed
case wfexec.SessionCanceled:
ses.SuspendedAt = nil
ses.CompletedAt = now()
ses.Status = types.SessionCanceled
default:
// force update every X iterations
update = ses.FlushCounter >= sessionStateFlushFrequency
if !update {
return
}
}
// Reset the counter whenever we flush due to whatever scenario
ses.FlushCounter = 0
ses.CopyRuntimeStacktrace()
if err := store.UpsertAutomationSession(ctx, svc.store, ses); err != nil {
log := svc.log.With(
logger.Uint64("sessionID", s.ID()),
zap.Stringer("status", status),
)
log.Error("failed to update session", zap.Error(err))
}
return
}
}
func loadSession(ctx context.Context, s store.Storer, sessionID uint64) (res *types.Session, err error) {
if sessionID == 0 {
return nil, SessionErrInvalidID()
}
if res, err = store.LookupAutomationSessionByID(ctx, s, sessionID); errors.IsNotFound(err) {
return nil, SessionErrNotFound()
}
return
}

View File

@@ -0,0 +1,638 @@
package service
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// automation/service/session_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strings"
"time"
)
type (
sessionActionProps struct {
session *types.Session
new *types.Session
update *types.Session
filter *types.SessionFilter
}
sessionAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *sessionActionProps
}
sessionLogMetaKey struct{}
sessionPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setSession updates sessionActionProps's session
//
// This function is auto-generated.
func (p *sessionActionProps) setSession(session *types.Session) *sessionActionProps {
p.session = session
return p
}
// setNew updates sessionActionProps's new
//
// This function is auto-generated.
func (p *sessionActionProps) setNew(new *types.Session) *sessionActionProps {
p.new = new
return p
}
// setUpdate updates sessionActionProps's update
//
// This function is auto-generated.
func (p *sessionActionProps) setUpdate(update *types.Session) *sessionActionProps {
p.update = update
return p
}
// setFilter updates sessionActionProps's filter
//
// This function is auto-generated.
func (p *sessionActionProps) setFilter(filter *types.SessionFilter) *sessionActionProps {
p.filter = filter
return p
}
// Serialize converts sessionActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p sessionActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.session != nil {
m.Set("session.ID", p.session.ID, true)
}
if p.new != nil {
m.Set("new.ID", p.new.ID, true)
}
if p.update != nil {
m.Set("update.ID", p.update.ID, true)
}
if p.filter != nil {
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p sessionActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.session != nil {
// replacement for "{{session}}" (in order how fields are defined)
pairs = append(
pairs,
"{{session}}",
fns(
p.session.ID,
),
)
pairs = append(pairs, "{{session.ID}}", fns(p.session.ID))
}
if p.new != nil {
// replacement for "{{new}}" (in order how fields are defined)
pairs = append(
pairs,
"{{new}}",
fns(
p.new.ID,
),
)
pairs = append(pairs, "{{new.ID}}", fns(p.new.ID))
}
if p.update != nil {
// replacement for "{{update}}" (in order how fields are defined)
pairs = append(
pairs,
"{{update}}",
fns(
p.update.ID,
),
)
pairs = append(pairs, "{{update.ID}}", fns(p.update.ID))
}
if p.filter != nil {
// replacement for "{{filter}}" (in order how fields are defined)
pairs = append(
pairs,
"{{filter}}",
fns(),
)
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *sessionAction) String() string {
var props = &sessionActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *sessionAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// SessionActionSearch returns "automation:session.search" action
//
// This function is auto-generated.
func SessionActionSearch(props ...*sessionActionProps) *sessionAction {
a := &sessionAction{
timestamp: time.Now(),
resource: "automation:session",
action: "search",
log: "searched for matching sessions",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SessionActionLookup returns "automation:session.lookup" action
//
// This function is auto-generated.
func SessionActionLookup(props ...*sessionActionProps) *sessionAction {
a := &sessionAction{
timestamp: time.Now(),
resource: "automation:session",
action: "lookup",
log: "looked-up for a {{session}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SessionActionCreate returns "automation:session.create" action
//
// This function is auto-generated.
func SessionActionCreate(props ...*sessionActionProps) *sessionAction {
a := &sessionAction{
timestamp: time.Now(),
resource: "automation:session",
action: "create",
log: "created {{session}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SessionActionUpdate returns "automation:session.update" action
//
// This function is auto-generated.
func SessionActionUpdate(props ...*sessionActionProps) *sessionAction {
a := &sessionAction{
timestamp: time.Now(),
resource: "automation:session",
action: "update",
log: "updated {{session}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SessionActionDelete returns "automation:session.delete" action
//
// This function is auto-generated.
func SessionActionDelete(props ...*sessionActionProps) *sessionAction {
a := &sessionAction{
timestamp: time.Now(),
resource: "automation:session",
action: "delete",
log: "deleted {{session}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// SessionActionUndelete returns "automation:session.undelete" action
//
// This function is auto-generated.
func SessionActionUndelete(props ...*sessionActionProps) *sessionAction {
a := &sessionAction{
timestamp: time.Now(),
resource: "automation:session",
action: "undelete",
log: "undeleted {{session}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// SessionErrGeneric returns "automation:session.generic" as *errors.Error
//
// This function is auto-generated.
func SessionErrGeneric(mm ...*sessionActionProps) *errors.Error {
var p = &sessionActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "automation:session"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sessionLogMetaKey{}, "{err}"),
errors.Meta(sessionPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "session.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SessionErrNotFound returns "automation:session.notFound" as *errors.Error
//
// This function is auto-generated.
func SessionErrNotFound(mm ...*sessionActionProps) *errors.Error {
var p = &sessionActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("session not found", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "automation:session"),
errors.Meta(sessionPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SessionErrInvalidID returns "automation:session.invalidID" as *errors.Error
//
// This function is auto-generated.
func SessionErrInvalidID(mm ...*sessionActionProps) *errors.Error {
var p = &sessionActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid ID", nil),
errors.Meta("type", "invalidID"),
errors.Meta("resource", "automation:session"),
errors.Meta(sessionPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "session.errors.invalidID"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SessionErrStaleData returns "automation:session.staleData" as *errors.Error
//
// This function is auto-generated.
func SessionErrStaleData(mm ...*sessionActionProps) *errors.Error {
var p = &sessionActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("session was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
errors.Meta("type", "staleData"),
errors.Meta("resource", "automation:session"),
errors.Meta(sessionPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "session.errors.staleData"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SessionErrNotAllowedToRead returns "automation:session.notAllowedToRead" as *errors.Error
//
// This function is auto-generated.
func SessionErrNotAllowedToRead(mm ...*sessionActionProps) *errors.Error {
var p = &sessionActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to read this session", nil),
errors.Meta("type", "notAllowedToRead"),
errors.Meta("resource", "automation:session"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sessionLogMetaKey{}, "failed to read {{session}}; insufficient permissions"),
errors.Meta(sessionPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notAllowedToRead"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SessionErrNotAllowedToSearch returns "automation:session.notAllowedToSearch" as *errors.Error
//
// This function is auto-generated.
func SessionErrNotAllowedToSearch(mm ...*sessionActionProps) *errors.Error {
var p = &sessionActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to search or list sessions", nil),
errors.Meta("type", "notAllowedToSearch"),
errors.Meta("resource", "automation:session"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sessionLogMetaKey{}, "failed to search or list session; insufficient permissions"),
errors.Meta(sessionPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notAllowedToSearch"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SessionErrNotAllowedToDelete returns "automation:session.notAllowedToDelete" as *errors.Error
//
// This function is auto-generated.
func SessionErrNotAllowedToDelete(mm ...*sessionActionProps) *errors.Error {
var p = &sessionActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to delete this session", nil),
errors.Meta("type", "notAllowedToDelete"),
errors.Meta("resource", "automation:session"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sessionLogMetaKey{}, "failed to delete {{session}}; insufficient permissions"),
errors.Meta(sessionPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notAllowedToDelete"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// SessionErrNotAllowedToManage returns "automation:session.notAllowedToManage" as *errors.Error
//
// This function is auto-generated.
func SessionErrNotAllowedToManage(mm ...*sessionActionProps) *errors.Error {
var p = &sessionActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to manage session's workflow", nil),
errors.Meta("type", "notAllowedToManage"),
errors.Meta("resource", "automation:session"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(sessionLogMetaKey{}, "failed to manage {{session}}; insufficient permissions"),
errors.Meta(sessionPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "session.errors.notAllowedToManage"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc session) recordAction(ctx context.Context, props *sessionActionProps, actionFn func(...*sessionActionProps) *sessionAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(sessionLogMetaKey{}), err)
if p, has := m[sessionPropsMetaKey{}]; has {
a.Meta = p.(*sessionActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,76 @@
# List of loggable service actions
resource: automation:session
service: session
import:
- github.com/cortezaproject/corteza/server/automation/types
# Default sensitivity for actions
defaultActionSeverity: info
# default severity for errors
defaultErrorSeverity: error
props:
- name: session
type: "*types.Session"
fields: [ ID ]
- name: new
type: "*types.Session"
fields: [ ID ]
- name: update
type: "*types.Session"
fields: [ ID ]
- name: filter
type: "*types.SessionFilter"
actions:
- action: search
log: "searched for matching sessions"
severity: info
- action: lookup
log: "looked-up for a {{session}}"
severity: info
- action: create
log: "created {{session}}"
- action: update
log: "updated {{session}}"
- action: delete
log: "deleted {{session}}"
- action: undelete
log: "undeleted {{session}}"
errors:
- error: notFound
message: "session not found"
- error: invalidID
message: "invalid ID"
- error: staleData
message: "session was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
severity: warning
- error: notAllowedToRead
message: "not allowed to read this session"
log: "failed to read {{session}}; insufficient permissions"
- error: notAllowedToSearch
message: "not allowed to search or list sessions"
log: "failed to search or list session; insufficient permissions"
- error: notAllowedToDelete
message: "not allowed to delete this session"
log: "failed to delete {{session}}; insufficient permissions"
- error: notAllowedToManage
message: "not allowed to manage session's workflow"
log: "failed to manage {{session}}; insufficient permissions"

View File

@@ -0,0 +1,234 @@
package service
import (
"context"
"testing"
"time"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
func TestSession_Start(t *testing.T) {
var (
req = require.New(t)
ses = &session{}
g = wfexec.NewGraph()
ctx = context.Background()
err error
)
_, _, err = ses.Start(ctx, g, types.SessionStartParams{Invoker: auth.Anonymous()})
req.EqualError(err, "could not find starting step")
g.AddStep(wfexec.NewGenericStep(nil))
_, _, err = ses.Start(ctx, g, types.SessionStartParams{StepID: 4321, Invoker: auth.Anonymous()})
req.EqualError(err, "trigger staring step references non-existing step")
// Adding another orphaned step and starting session w/o explicitly specifying the starting step
g.AddStep(wfexec.NewGenericStep(nil))
_, _, err = ses.Start(ctx, g, types.SessionStartParams{Invoker: auth.Anonymous()})
req.EqualError(err, "cannot start workflow session multiple starting steps found")
// add a generic step with a known ID so we can use it as a starting point
s := wfexec.NewGenericStep(nil)
s.SetID(42)
g.AddStep(s)
// add parents to the 42 step
g.AddStep(wfexec.NewGenericStep(nil), s)
_, _, err = ses.Start(ctx, g, types.SessionStartParams{StepID: 42, Invoker: auth.Anonymous()})
req.EqualError(err, "cannot start workflow on a step with parents")
}
func TestSessionStackTraces(t *testing.T) {
stepID := uint64(0)
getStepID := func() uint64 {
stepID++
return uint64(stepID)
}
iter1 := &wfexec.Frame{StepID: getStepID(), ParentID: 0, Action: "iterator initialized"}
iter1stepExpr := &wfexec.Frame{StepID: getStepID(), ParentID: iter1.StepID, Action: ""}
iter1stepCnt := &wfexec.Frame{StepID: getStepID(), ParentID: iter1stepExpr.StepID, Action: "loop continue"}
iter2 := &wfexec.Frame{StepID: getStepID(), ParentID: iter1.StepID, Action: "iterator initialized"}
iter2stepExpr := &wfexec.Frame{StepID: getStepID(), ParentID: iter2.StepID, Action: ""}
iter2stepCnt := &wfexec.Frame{StepID: getStepID(), ParentID: iter2stepExpr.StepID, Action: "loop continue"}
rando1 := &wfexec.Frame{StepID: getStepID(), ParentID: iter2.StepID, Action: ""}
rando2 := &wfexec.Frame{StepID: getStepID(), ParentID: iter2.StepID, Action: ""}
trace := []*wfexec.Frame{
// first loop
iter1,
iter1stepExpr,
iter1stepCnt,
iter1,
iter1stepExpr,
iter1stepCnt,
iter1,
// second loop
iter2,
iter2stepExpr,
iter2stepCnt,
iter2,
iter2stepExpr,
iter2stepCnt,
iter2,
rando1,
rando2,
}
ses := &types.Session{}
for _, f := range trace {
ses.AppendRuntimeStacktrace(f)
}
require.Len(t, ses.RuntimeStacktrace, 10)
require.Equal(t, iter1.StepID, ses.RuntimeStacktrace[0].StepID)
require.Equal(t, iter1stepExpr.StepID, ses.RuntimeStacktrace[1].StepID)
require.Equal(t, iter1stepCnt.StepID, ses.RuntimeStacktrace[2].StepID)
require.Equal(t, iter1.StepID, ses.RuntimeStacktrace[3].StepID)
require.Equal(t, iter2.StepID, ses.RuntimeStacktrace[4].StepID)
require.Equal(t, iter2stepExpr.StepID, ses.RuntimeStacktrace[5].StepID)
require.Equal(t, iter2stepCnt.StepID, ses.RuntimeStacktrace[6].StepID)
require.Equal(t, iter2.StepID, ses.RuntimeStacktrace[7].StepID)
require.Equal(t, rando1.StepID, ses.RuntimeStacktrace[8].StepID)
require.Equal(t, rando2.StepID, ses.RuntimeStacktrace[9].StepID)
}
// Before:
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/automation/service
// BenchmarkSessionStackTraces_1000-12 42758 28594 ns/op 87288 B/op 15 allocs/op
// BenchmarkSessionStackTraces_10000-12 3430 348308 ns/op 1160442 B/op 23 allocs/op
// BenchmarkSessionStackTraces_100000-12 229 5299188 ns/op 13317368 B/op 33 allocs/op
// BenchmarkSessionStackTraces_1000000-12 19 52896750 ns/op 128431352 B/op 43 allocs/op
// BenchmarkSessionStackTraces_10000000-12 3 436311292 ns/op 1202337018 B/op 53 allocs/op
// After:
// goos: darwin
// goarch: arm64
// pkg: github.com/cortezaproject/corteza/server/automation/service
// BenchmarkSessionStackTraces_1000-12 40142 29860 ns/op 120 B/op 4 allocs/op
// BenchmarkSessionStackTraces_10000-12 4387 264930 ns/op 120 B/op 4 allocs/op
// BenchmarkSessionStackTraces_100000-12 447 2735284 ns/op 120 B/op 4 allocs/op
// BenchmarkSessionStackTraces_1000000-12 42 26596636 ns/op 120 B/op 4 allocs/op
// BenchmarkSessionStackTraces_10000000-12 4 265553667 ns/op 120 B/op 4 allocs/op
func benchmarkSessionStackTraces(b *testing.B, iters int) {
stepID := uint64(0)
getStepID := func() uint64 {
stepID++
return uint64(stepID)
}
iter := &wfexec.Frame{
StepID: getStepID(),
Action: "iterator initialized",
}
loop := []*wfexec.Frame{iter}
loop = append(loop, &wfexec.Frame{
StepID: getStepID(),
ParentID: iter.ParentID,
Action: "",
})
loop = append(loop, &wfexec.Frame{
StepID: getStepID(),
ParentID: loop[len(loop)-1].StepID,
Action: "loop continue",
})
frames := []*wfexec.Frame{}
for x := 0; x < iters; x++ {
frames = append(frames, loop...)
}
frames = append(frames, iter)
frames = append(frames, &wfexec.Frame{
StepID: getStepID(),
ParentID: iter.ParentID,
Action: "termination",
})
b.StopTimer()
b.ResetTimer()
for n := 0; n < b.N; n++ {
ses := &types.Session{}
b.StartTimer()
for _, f := range frames {
ses.AppendRuntimeStacktrace(f)
}
b.StopTimer()
}
}
func BenchmarkSessionStackTraces_1000(b *testing.B) {
benchmarkSessionStackTraces(b, 1000)
}
func BenchmarkSessionStackTraces_10000(b *testing.B) {
benchmarkSessionStackTraces(b, 10000)
}
func BenchmarkSessionStackTraces_100000(b *testing.B) {
benchmarkSessionStackTraces(b, 100000)
}
func BenchmarkSessionStackTraces_1000000(b *testing.B) {
benchmarkSessionStackTraces(b, 1000000)
}
func BenchmarkSessionStackTraces_10000000(b *testing.B) {
benchmarkSessionStackTraces(b, 10000000)
}
func TestSession_logPending(t *testing.T) {
var (
req = require.New(t)
core, o = observer.New(zap.NewAtomicLevelAt(zap.InfoLevel))
ref = time.Now()
nextSesID uint64
mkSes = func(age time.Duration) *types.Session {
nextSesID++
return &types.Session{ID: nextSesID, CreatedAt: ref.Add(-age)}
}
svc = &session{
log: zap.New(core),
pool: map[uint64]*types.Session{
1: mkSes(time.Second * 5),
2: mkSes(time.Minute * 5),
3: mkSes(time.Hour * 5),
4: mkSes(time.Hour * 30),
5: mkSes(time.Hour * 72),
},
}
)
// pin the clock so bucketing does not depend on test runtime
now = func() *time.Time { return &ref }
defer func() { now = func() *time.Time { c := time.Now(); return &c } }()
svc.logPending()
entries := o.All()
req.Len(entries, 1)
fields := entries[0].ContextMap()
req.EqualValues(5, fields["total"])
req.EqualValues(1, fields["pending"])
req.EqualValues(1, fields["pending1m"])
req.EqualValues(1, fields["pending1h"])
req.EqualValues(2, fields["pending1d"])
}

View File

@@ -0,0 +1,728 @@
package service
import (
"context"
"fmt"
"reflect"
"sync"
"github.com/cortezaproject/corteza/server/automation/types"
cmpEvent "github.com/cortezaproject/corteza/server/compose/service/event"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/label"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/store"
sysEvent "github.com/cortezaproject/corteza/server/system/service/event"
"go.uber.org/zap"
)
type (
trigger struct {
eventbus triggerEventTriggerHandler
store store.Storer
actionlog actionlog.Recorder
ac triggerAccessController
opt options.WorkflowOpt
log *zap.Logger
// maps registered triggers (value, uintptr) to trigger ID (key, uint64)
// this will keep track of all our trigger registrations and help us do a cleanup on
// trigger update.
triggers map[uint64]uintptr
reg map[uint64]map[uint64]uintptr
workflow *workflow
session *session
mux *sync.RWMutex
}
triggerAccessController interface {
CanSearchTriggers(context.Context) bool
CanManageTriggersOnWorkflow(context.Context, *types.Workflow) bool
CanExecuteWorkflow(context.Context, *types.Workflow) bool
}
triggerEventTriggerHandler interface {
Register(h eventbus.HandlerFn, ops ...eventbus.HandlerRegOp) uintptr
Unregister(ptrs ...uintptr)
}
triggerUpdateHandler func(ctx context.Context, ns *types.Trigger) (triggerChanges, error)
triggerChanges uint8
varsEncoder interface {
EncodeVars() (*expr.Vars, error)
}
varsDecoder interface {
DecodeVars(*expr.Vars) error
}
)
const (
triggerUnchanged triggerChanges = 0
triggerChanged triggerChanges = 1
triggerLabelsChanged triggerChanges = 2
)
func Trigger(log *zap.Logger, opt options.WorkflowOpt) *trigger {
return &trigger{
log: log,
opt: opt,
eventbus: eventbus.Service(),
actionlog: DefaultActionlog,
store: DefaultStore,
ac: DefaultAccessControl,
session: DefaultSession,
workflow: DefaultWorkflow,
triggers: make(map[uint64]uintptr),
reg: make(map[uint64]map[uint64]uintptr),
mux: &sync.RWMutex{},
}
}
func (svc *trigger) Search(ctx context.Context, filter types.TriggerFilter) (rr types.TriggerSet, f types.TriggerFilter, err error) {
var (
wap = &triggerActionProps{filter: &filter}
)
err = func() (err error) {
if !svc.ac.CanSearchTriggers(ctx) {
return TriggerErrNotAllowedToSearch()
}
if len(filter.Labels) > 0 {
filter.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.Trigger{}.LabelResourceKind(),
filter.Labels,
)
if err != nil {
return err
}
// labels specified but no labeled resources found
if len(filter.LabeledIDs) == 0 {
return nil
}
}
if rr, f, err = store.SearchAutomationTriggers(ctx, svc.store, filter); err != nil {
return err
}
if err = label.Load(ctx, svc.store, toLabeledTriggers(rr)...); err != nil {
return err
}
return nil
}()
return rr, filter, svc.recordAction(ctx, wap, TriggerActionSearch, err)
}
// SearchOnManual finds first matching onManual trigger and returns it
//
// In case stepID is 0, first trigger is returned
func (svc *trigger) SearchOnManual(ctx context.Context, workflowID, stepID uint64) (*types.Trigger, error) {
tt, _, err := svc.Search(ctx, types.TriggerFilter{
WorkflowID: id.Strings(workflowID),
EventType: "onManual",
})
if err != nil {
return nil, err
}
if stepID == 0 && len(tt) > 0 {
return tt[0], nil
}
for _, t := range tt {
if t.StepID == stepID {
return t, nil
}
}
return nil, nil
}
func (svc *trigger) LookupByID(ctx context.Context, triggerID uint64) (res *types.Trigger, err error) {
var (
wap = &triggerActionProps{trigger: &types.Trigger{ID: triggerID}}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
if !svc.ac.CanSearchTriggers(ctx) {
return TriggerErrNotAllowedToRead()
}
if res, err = loadTrigger(ctx, s, triggerID); err != nil {
return err
}
if err = label.Load(ctx, svc.store, res); err != nil {
return err
}
return nil
})
return res, svc.recordAction(ctx, wap, TriggerActionLookup, err)
}
// Create adds new trigger resource and saves it into store
// It updates service's cache
func (svc *trigger) Create(ctx context.Context, new *types.Trigger) (res *types.Trigger, err error) {
var (
wap = &triggerActionProps{new: new}
cUser = auth.GetIdentityFromContext(ctx).Identity()
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
var (
wf *types.Workflow
)
if wf, err = loadWorkflow(ctx, svc.store, new.WorkflowID); err != nil {
return err
}
if !svc.ac.CanManageTriggersOnWorkflow(ctx, wf) {
return TriggerErrNotAllowedToCreate()
}
res = &types.Trigger{
ID: nextID(),
Enabled: new.Enabled,
WorkflowID: new.WorkflowID,
StepID: new.StepID,
ResourceType: new.ResourceType,
EventType: new.EventType,
Constraints: new.Constraints,
Input: new.Input,
Labels: new.Labels,
Meta: new.Meta,
OwnedBy: cUser,
CreatedAt: *now(),
CreatedBy: cUser,
}
wap.new = res
if err = store.CreateAutomationTrigger(ctx, s, res); err != nil {
return
}
if err = label.Create(ctx, s, res); err != nil {
return
}
// Ignore workflow issues as those are defined by the workflow itself.
// Internal errors should still be reported.
if err = svc.registerWorkflow(ctx, wf, res); err != nil {
if _, ok := err.(types.WorkflowIssueSet); ok {
err = nil
}
}
return
})
return res, svc.recordAction(ctx, wap, TriggerActionCreate, err)
}
// Update modifies existing trigger resource in the store
func (svc *trigger) Update(ctx context.Context, upd *types.Trigger) (*types.Trigger, error) {
return svc.updater(ctx, upd.ID, TriggerActionUpdate, func(ctx context.Context, res *types.Trigger) (triggerChanges, error) {
if err := svc.canManageTrigger(ctx, res, TriggerErrNotAllowedToUpdate()); err != nil {
return triggerUnchanged, err
}
handler := svc.handleUpdate(upd)
return handler(ctx, res)
})
}
func (svc *trigger) DeleteByID(ctx context.Context, triggerID uint64) error {
return trim1st(svc.updater(ctx, triggerID, TriggerActionDelete, svc.handleDelete))
}
func (svc *trigger) UndeleteByID(ctx context.Context, triggerID uint64) error {
return trim1st(svc.updater(ctx, triggerID, TriggerActionUndelete, svc.handleUndelete))
}
func (svc trigger) updater(ctx context.Context, triggerID uint64, action func(...*triggerActionProps) *triggerAction, fn triggerUpdateHandler) (*types.Trigger, error) {
var (
changes triggerChanges
res *types.Trigger
aProps = &triggerActionProps{trigger: &types.Trigger{ID: triggerID}}
err error
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
res, err = loadTrigger(ctx, s, triggerID)
if err != nil {
return
}
if err = label.Load(ctx, svc.store, res); err != nil {
return err
}
aProps.setTrigger(res)
aProps.setUpdate(res)
if changes, err = fn(ctx, res); err != nil {
return err
}
if changes&triggerChanged > 0 {
if err = store.UpdateAutomationTrigger(ctx, svc.store, res); err != nil {
return err
}
}
if changes&triggerLabelsChanged > 0 {
if err = label.Update(ctx, s, res); err != nil {
return
}
}
return err
})
return res, svc.recordAction(ctx, aProps, action, err)
}
func (svc trigger) handleUpdate(upd *types.Trigger) triggerUpdateHandler {
return func(ctx context.Context, res *types.Trigger) (changes triggerChanges, err error) {
if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) {
return triggerUnchanged, TriggerErrStaleData()
}
if res.Enabled != upd.Enabled {
changes |= triggerChanged
res.Enabled = upd.Enabled
}
if upd.Labels != nil {
if label.Changed(res.Labels, upd.Labels) {
changes |= triggerLabelsChanged
res.Labels = upd.Labels
}
}
if res.StepID != upd.StepID {
changes |= triggerChanged
res.StepID = upd.StepID
}
if res.EventType != upd.EventType {
changes |= triggerChanged
res.EventType = upd.EventType
}
if res.ResourceType != upd.ResourceType {
changes |= triggerChanged
res.ResourceType = upd.ResourceType
}
if upd.Meta != nil {
if !reflect.DeepEqual(upd.Meta, res.Meta) {
changes |= triggerChanged
res.Meta = upd.Meta
}
}
if upd.Input != nil {
if !reflect.DeepEqual(upd.Input, res.Input) {
changes |= triggerChanged
res.Input = upd.Input
}
}
if upd.Constraints != nil {
if !reflect.DeepEqual(upd.Constraints, res.Constraints) {
changes |= triggerChanged
res.Constraints = upd.Constraints
}
}
if res.OwnedBy != upd.OwnedBy {
// @todo need to check against access control if current user can modify owner
changes |= triggerChanged
res.OwnedBy = upd.OwnedBy
}
if changes&triggerChanged > 0 {
res.UpdatedAt = now()
}
return
}
}
func (svc trigger) handleDelete(ctx context.Context, res *types.Trigger) (triggerChanges, error) {
if err := svc.canManageTrigger(ctx, res, TriggerErrNotAllowedToDelete()); err != nil {
return triggerUnchanged, err
}
if res.DeletedAt != nil {
// trigger already deleted
return triggerUnchanged, nil
}
res.DeletedAt = now()
return triggerChanged, nil
}
func (svc trigger) handleUndelete(ctx context.Context, res *types.Trigger) (triggerChanges, error) {
if err := svc.canManageTrigger(ctx, res, TriggerErrNotAllowedToUndelete()); err != nil {
return triggerUnchanged, err
}
if res.DeletedAt == nil {
// trigger not deleted
return triggerUnchanged, nil
}
res.DeletedAt = nil
return triggerChanged, nil
}
func (svc trigger) canManageTrigger(ctx context.Context, res *types.Trigger, permErr error) error {
if wf, err := loadWorkflow(ctx, svc.store, res.WorkflowID); err != nil {
return err
} else if !svc.ac.CanManageTriggersOnWorkflow(ctx, wf) {
return permErr
} else {
return nil
}
}
// registers all triggers on all given workflows
// before registering triggers on a workflow, all workflow triggers are unregistered
func (svc *trigger) registerWorkflows(ctx context.Context, workflows ...*types.Workflow) error {
// load ALL triggers directly from store
tt, _, err := store.SearchAutomationTriggers(ctx, svc.store, types.TriggerFilter{
WorkflowID: id.Strings(types.WorkflowSet(workflows).IDs()...),
Deleted: filter.StateInclusive,
Disabled: filter.StateExcluded,
})
if err != nil {
return err
}
for _, wf := range workflows {
svc.unregisterWorkflows(wf)
if !wf.Enabled {
continue
}
if wf.DeletedAt != nil {
continue
}
if len(wf.Issues) > 0 {
// workflow was processed before and issues were detected
// and stored on the workflow; no need to continue
continue
}
if err = svc.registerWorkflow(ctx, wf, tt.FilterByWorkflowID(wf.ID)...); err != nil {
return err
}
}
return nil
}
// updates trigger handler registration
//
// Loads associated workflow and registers specific trigger
func (svc *trigger) updateTriggerRegistration(ctx context.Context, t *types.Trigger) error {
wf, err := loadWorkflow(ctx, svc.store, t.WorkflowID)
if err != nil {
return err
}
return svc.registerWorkflow(ctx, wf, t)
}
// registers one workflow and a set of triggers
func (svc *trigger) registerWorkflow(ctx context.Context, wf *types.Workflow, tt ...*types.Trigger) (err error) {
var (
runAs auth.Identifiable
)
// Returns context with identity set to service user
//
// Current user (identity in the context) might not have
// sufficient privileges to load info about invoker and runner
sysUserCtx := func() context.Context {
return auth.SetIdentityToContext(ctx, auth.ServiceUser())
}
if !svc.opt.Register {
return nil
}
if !wf.Enabled || len(wf.Issues) > 0 {
// do not even try to register disabled
// workflows or workflows with issues
return nil
}
if len(types.TriggerSet(tt).FilterByWorkflowID(wf.ID)) < len(tt) {
return fmt.Errorf("all triggers must reference the given workflow")
}
if wis := validateWorkflowTriggers(wf, tt...); len(wis) > 0 {
// skip trigger registration of there is a trigger related issue(s)
// on a specific workflow.
//
// this really happens since we run all validation on save,
// but there might be workflow from the time when these checks were
// not in place
return nil
}
if wf.RunAs > 0 {
if runAs, err = DefaultUser.FindByAny(sysUserCtx(), wf.RunAs); err != nil {
return fmt.Errorf("failed to load run-as user %d: %w", wf.RunAs, err)
} else if !runAs.Valid() {
return fmt.Errorf("invalid user %d used for workflow run-as", wf.RunAs)
}
}
svc.registerTriggers(wf, runAs, tt...)
return nil
}
// registerTriggers registers workflows triggers to eventbus
//
// It preloads run-as identity and finds a starting step for each trigger
func (svc *trigger) registerTriggers(wf *types.Workflow, runAs auth.Identifiable, tt ...*types.Trigger) {
var (
handlerFn eventbus.HandlerFn
err error
g *wfexec.Graph
issues types.WorkflowIssueSet
wfLog = svc.log.
With(logger.Uint64("workflowID", wf.ID))
// register only enabled, undeleted workflows
registerWorkflow = wf.Enabled && wf.DeletedAt == nil
)
// convert only registrable and workflows without issues
if registerWorkflow && len(wf.Issues) == 0 {
// Convert workflow only when valid (no issues, enable, not delete)
if g, issues = Convert(svc.workflow, wf); len(issues) > 0 {
wfLog.Error("failed to convert workflow to graph", zap.Error(issues))
_ = issues.Walk(func(i *types.WorkflowIssue) error {
wfLog.Debug("workflow issue found: "+i.Description, zap.Any("culprit", i.Culprit))
return nil
})
g = nil
}
}
defer svc.mux.Unlock()
svc.mux.Lock()
for _, t := range tt {
log := wfLog.With(logger.Uint64("triggerID", t.ID))
// always unregister
if svc.reg[wf.ID] == nil {
svc.reg[wf.ID] = make(map[uint64]uintptr)
} else if ptr := svc.reg[wf.ID][t.ID]; ptr != 0 {
// unregister handlers for this trigger if they exist
svc.eventbus.Unregister(ptr)
}
// do not register disabled or deleted triggers
if !registerWorkflow || !t.Enabled || t.DeletedAt != nil {
continue
}
var (
cnstr eventbus.ConstraintMatcher
ops = make([]eventbus.HandlerRegOp, 0, len(t.Constraints)+2)
)
if g == nil {
handlerFn = func(_ context.Context, ev eventbus.Event) error {
return errors.Internal(
"trigger %s on %s failed due to invalid workflow %d: %s",
ev.EventType(),
ev.ResourceType(),
wf.ID,
wf.Issues,
).Wrap(wf.Issues)
}
} else {
handlerFn = makeWorkflowHandler(svc.workflow, wf, t)
}
ops = append(
ops,
eventbus.On(t.EventType),
eventbus.For(t.ResourceType),
)
for _, c := range t.Constraints {
if cnstr, err = eventbus.ConstraintMaker(c.Name, c.Op, c.Values...); err != nil {
log.Debug(
"failed to make constraint for workflow trigger",
zap.Any("constraint", c),
zap.Error(err),
)
} else {
ops = append(ops, eventbus.Constraint(cnstr))
}
}
svc.reg[wf.ID][t.ID] = svc.eventbus.Register(handlerFn, ops...)
log.Debug("trigger registered",
zap.String("eventType", t.EventType),
zap.String("resourceType", t.ResourceType),
zap.Any("constraints", t.Constraints),
)
}
}
func (svc *trigger) unregisterWorkflows(wwf ...*types.Workflow) {
defer svc.mux.Unlock()
svc.mux.Lock()
for _, wf := range wwf {
for triggerID, ptr := range svc.reg[wf.ID] {
svc.eventbus.Unregister(ptr)
svc.log.Debug("trigger unregistered", logger.Uint64("triggerID", triggerID), logger.Uint64("workflowID", wf.ID))
delete(svc.triggers, wf.ID)
}
delete(svc.reg, wf.ID)
}
}
func (svc *trigger) unregisterTriggers(tt ...*types.Trigger) {
defer svc.mux.Unlock()
svc.mux.Lock()
for _, t := range tt {
if svc.reg[t.WorkflowID] == nil {
return
}
if ptr, has := svc.reg[t.WorkflowID][t.ID]; has {
svc.eventbus.Unregister(ptr)
svc.log.Debug("trigger unregistered", logger.Uint64("triggerID", t.ID), logger.Uint64("workflowID", t.WorkflowID))
delete(svc.triggers, t.ID)
}
}
}
func loadTrigger(ctx context.Context, s store.Storer, triggerID uint64) (res *types.Trigger, err error) {
if triggerID == 0 {
return nil, TriggerErrInvalidID()
}
if res, err = store.LookupAutomationTriggerByID(ctx, s, triggerID); errors.IsNotFound(err) {
return nil, TriggerErrNotFound()
}
return
}
func loadWorkflowTriggers(ctx context.Context, s store.Storer, workflowID uint64) (tt types.TriggerSet, err error) {
if workflowID == 0 {
return nil, TriggerErrInvalidID()
}
if tt, _, err = store.SearchAutomationTriggers(ctx, s, types.TriggerFilter{WorkflowID: id.Strings(workflowID)}); errors.IsNotFound(err) {
return nil, TriggerErrNotFound()
}
return
}
// toLabeledTriggers converts to []label.LabeledResource
func toLabeledTriggers(set []*types.Trigger) []label.LabeledResource {
if len(set) == 0 {
return nil
}
ll := make([]label.LabeledResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}
// Checks if triggers are compatible with the workflow
//
// It ignores disabled triggers and does not care if triggers are in fact bond to the
// given workflow
func validateWorkflowTriggers(wf *types.Workflow, tt ...*types.Trigger) (wis types.WorkflowIssueSet) {
var (
// @todo find a better way how to flag events type that require
// run-as param to be set
// Possible solution: flag in definition that generates static
// list w/o the need of cross-component imports
requireRunAs = []eventbus.Event{
sysEvent.SinkOnRequest(nil, nil),
sysEvent.QueueOnMessage(nil),
sysEvent.SystemOnInterval(),
sysEvent.SystemOnTimestamp(),
cmpEvent.ComposeOnInterval(),
cmpEvent.ComposeOnTimestamp(),
}
)
for i, t := range tt {
if !t.Enabled || t.DeletedAt != nil {
continue
}
for _, ev := range requireRunAs {
if t.ResourceType == ev.ResourceType() && t.EventType == ev.EventType() {
if wf.RunAs == 0 {
wis = wis.Append(
errors.InvalidData("%s for %s requires run-as to be set", ev.ResourceType(), ev.EventType()),
map[string]int{"trigger": i},
)
}
}
}
if wf.Meta != nil && wf.Meta.SubWorkflow {
wis = wis.Append(
errors.InvalidData("workflow marked as sub-workflow cannot have enabled triggers"),
map[string]int{"trigger": i},
)
}
}
return
}

View File

@@ -0,0 +1,706 @@
package service
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// automation/service/trigger_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strings"
"time"
)
type (
triggerActionProps struct {
trigger *types.Trigger
new *types.Trigger
update *types.Trigger
filter *types.TriggerFilter
}
triggerAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *triggerActionProps
}
triggerLogMetaKey struct{}
triggerPropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setTrigger updates triggerActionProps's trigger
//
// This function is auto-generated.
func (p *triggerActionProps) setTrigger(trigger *types.Trigger) *triggerActionProps {
p.trigger = trigger
return p
}
// setNew updates triggerActionProps's new
//
// This function is auto-generated.
func (p *triggerActionProps) setNew(new *types.Trigger) *triggerActionProps {
p.new = new
return p
}
// setUpdate updates triggerActionProps's update
//
// This function is auto-generated.
func (p *triggerActionProps) setUpdate(update *types.Trigger) *triggerActionProps {
p.update = update
return p
}
// setFilter updates triggerActionProps's filter
//
// This function is auto-generated.
func (p *triggerActionProps) setFilter(filter *types.TriggerFilter) *triggerActionProps {
p.filter = filter
return p
}
// Serialize converts triggerActionProps to actionlog.Meta
//
// This function is auto-generated.
func (p triggerActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.trigger != nil {
m.Set("trigger.ID", p.trigger.ID, true)
}
if p.new != nil {
m.Set("new.ID", p.new.ID, true)
}
if p.update != nil {
m.Set("update.ID", p.update.ID, true)
}
if p.filter != nil {
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
func (p triggerActionProps) Format(in string, err error) string {
var (
pairs = []string{"{{err}}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.trigger != nil {
// replacement for "{{trigger}}" (in order how fields are defined)
pairs = append(
pairs,
"{{trigger}}",
fns(
p.trigger.ID,
),
)
pairs = append(pairs, "{{trigger.ID}}", fns(p.trigger.ID))
}
if p.new != nil {
// replacement for "{{new}}" (in order how fields are defined)
pairs = append(
pairs,
"{{new}}",
fns(
p.new.ID,
),
)
pairs = append(pairs, "{{new.ID}}", fns(p.new.ID))
}
if p.update != nil {
// replacement for "{{update}}" (in order how fields are defined)
pairs = append(
pairs,
"{{update}}",
fns(
p.update.ID,
),
)
pairs = append(pairs, "{{update.ID}}", fns(p.update.ID))
}
if p.filter != nil {
// replacement for "{{filter}}" (in order how fields are defined)
pairs = append(
pairs,
"{{filter}}",
fns(),
)
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
func (a *triggerAction) String() string {
var props = &triggerActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *triggerAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// TriggerActionSearch returns "automation:trigger.search" action
//
// This function is auto-generated.
func TriggerActionSearch(props ...*triggerActionProps) *triggerAction {
a := &triggerAction{
timestamp: time.Now(),
resource: "automation:trigger",
action: "search",
log: "searched for matching triggers",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TriggerActionLookup returns "automation:trigger.lookup" action
//
// This function is auto-generated.
func TriggerActionLookup(props ...*triggerActionProps) *triggerAction {
a := &triggerAction{
timestamp: time.Now(),
resource: "automation:trigger",
action: "lookup",
log: "looked-up for a {{trigger}}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TriggerActionCreate returns "automation:trigger.create" action
//
// This function is auto-generated.
func TriggerActionCreate(props ...*triggerActionProps) *triggerAction {
a := &triggerAction{
timestamp: time.Now(),
resource: "automation:trigger",
action: "create",
log: "created {{trigger}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TriggerActionUpdate returns "automation:trigger.update" action
//
// This function is auto-generated.
func TriggerActionUpdate(props ...*triggerActionProps) *triggerAction {
a := &triggerAction{
timestamp: time.Now(),
resource: "automation:trigger",
action: "update",
log: "updated {{trigger}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TriggerActionDelete returns "automation:trigger.delete" action
//
// This function is auto-generated.
func TriggerActionDelete(props ...*triggerActionProps) *triggerAction {
a := &triggerAction{
timestamp: time.Now(),
resource: "automation:trigger",
action: "delete",
log: "deleted {{trigger}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TriggerActionUndelete returns "automation:trigger.undelete" action
//
// This function is auto-generated.
func TriggerActionUndelete(props ...*triggerActionProps) *triggerAction {
a := &triggerAction{
timestamp: time.Now(),
resource: "automation:trigger",
action: "undelete",
log: "undeleted {{trigger}}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// TriggerErrGeneric returns "automation:trigger.generic" as *errors.Error
//
// This function is auto-generated.
func TriggerErrGeneric(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "automation:trigger"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(triggerLogMetaKey{}, "{err}"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.generic"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrNotFound returns "automation:trigger.notFound" as *errors.Error
//
// This function is auto-generated.
func TriggerErrNotFound(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("trigger not found", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "automation:trigger"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notFound"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrInvalidID returns "automation:trigger.invalidID" as *errors.Error
//
// This function is auto-generated.
func TriggerErrInvalidID(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid ID", nil),
errors.Meta("type", "invalidID"),
errors.Meta("resource", "automation:trigger"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.invalidID"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrStaleData returns "automation:trigger.staleData" as *errors.Error
//
// This function is auto-generated.
func TriggerErrStaleData(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("trigger was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version", nil),
errors.Meta("type", "staleData"),
errors.Meta("resource", "automation:trigger"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.staleData"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrNotAllowedToRead returns "automation:trigger.notAllowedToRead" as *errors.Error
//
// This function is auto-generated.
func TriggerErrNotAllowedToRead(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to read this trigger", nil),
errors.Meta("type", "notAllowedToRead"),
errors.Meta("resource", "automation:trigger"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(triggerLogMetaKey{}, "failed to read {{trigger.ID}}; insufficient permissions"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToRead"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrNotAllowedToSearch returns "automation:trigger.notAllowedToSearch" as *errors.Error
//
// This function is auto-generated.
func TriggerErrNotAllowedToSearch(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to search or list triggers", nil),
errors.Meta("type", "notAllowedToSearch"),
errors.Meta("resource", "automation:trigger"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(triggerLogMetaKey{}, "failed to search or list trigger; insufficient permissions"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToSearch"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrNotAllowedToCreate returns "automation:trigger.notAllowedToCreate" as *errors.Error
//
// This function is auto-generated.
func TriggerErrNotAllowedToCreate(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to create triggers", nil),
errors.Meta("type", "notAllowedToCreate"),
errors.Meta("resource", "automation:trigger"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(triggerLogMetaKey{}, "failed to create trigger; insufficient permissions"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToCreate"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrNotAllowedToUpdate returns "automation:trigger.notAllowedToUpdate" as *errors.Error
//
// This function is auto-generated.
func TriggerErrNotAllowedToUpdate(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to update this trigger", nil),
errors.Meta("type", "notAllowedToUpdate"),
errors.Meta("resource", "automation:trigger"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(triggerLogMetaKey{}, "failed to update {{trigger.ID}}; insufficient permissions"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToUpdate"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrNotAllowedToDelete returns "automation:trigger.notAllowedToDelete" as *errors.Error
//
// This function is auto-generated.
func TriggerErrNotAllowedToDelete(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to delete this trigger", nil),
errors.Meta("type", "notAllowedToDelete"),
errors.Meta("resource", "automation:trigger"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(triggerLogMetaKey{}, "failed to delete {{trigger.ID}}; insufficient permissions"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToDelete"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TriggerErrNotAllowedToUndelete returns "automation:trigger.notAllowedToUndelete" as *errors.Error
//
// This function is auto-generated.
func TriggerErrNotAllowedToUndelete(mm ...*triggerActionProps) *errors.Error {
var p = &triggerActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to undelete this trigger", nil),
errors.Meta("type", "notAllowedToUndelete"),
errors.Meta("resource", "automation:trigger"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(triggerLogMetaKey{}, "failed to undelete {{trigger.ID}}; insufficient permissions"),
errors.Meta(triggerPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "automation"),
errors.Meta(locale.ErrorMetaKey{}, "trigger.errors.notAllowedToUndelete"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
func (svc trigger) recordAction(ctx context.Context, props *triggerActionProps, actionFn func(...*triggerActionProps) *triggerAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(triggerLogMetaKey{}), err)
if p, has := m[triggerPropsMetaKey{}]; has {
a.Meta = p.(*triggerActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,84 @@
# List of loggable service actions
resource: automation:trigger
service: trigger
import:
- github.com/cortezaproject/corteza/server/automation/types
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
props:
- name: trigger
type: "*types.Trigger"
fields: [ ID ]
- name: new
type: "*types.Trigger"
fields: [ ID ]
- name: update
type: "*types.Trigger"
fields: [ ID ]
- name: filter
type: "*types.TriggerFilter"
actions:
- action: search
log: "searched for matching triggers"
severity: info
- action: lookup
log: "looked-up for a {{trigger}}"
severity: info
- action: create
log: "created {{trigger}}"
- action: update
log: "updated {{trigger}}"
- action: delete
log: "deleted {{trigger}}"
- action: undelete
log: "undeleted {{trigger}}"
errors:
- error: notFound
message: "trigger not found"
- error: invalidID
message: "invalid ID"
- error: staleData
message: "trigger was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
severity: warning
- error: notAllowedToRead
message: "not allowed to read this trigger"
log: "failed to read {{trigger.ID}}; insufficient permissions"
- error: notAllowedToSearch
message: "not allowed to search or list triggers"
log: "failed to search or list trigger; insufficient permissions"
- error: notAllowedToCreate
message: "not allowed to create triggers"
log: "failed to create trigger; insufficient permissions"
- error: notAllowedToUpdate
message: "not allowed to update this trigger"
log: "failed to update {{trigger.ID}}; insufficient permissions"
- error: notAllowedToDelete
message: "not allowed to delete this trigger"
log: "failed to delete {{trigger.ID}}; insufficient permissions"
- error: notAllowedToUndelete
message: "not allowed to undelete this trigger"
log: "failed to undelete {{trigger.ID}}; insufficient permissions"

View File

@@ -0,0 +1,52 @@
package service
import (
"github.com/cortezaproject/corteza/server/automation/types"
sysEvent "github.com/cortezaproject/corteza/server/system/service/event"
"github.com/stretchr/testify/require"
"testing"
)
func TestValidateWorkflowTriggersEmpty(t *testing.T) {
var (
req = require.New(t)
issues = validateWorkflowTriggers(
&types.Workflow{},
)
)
req.Empty(issues)
}
func TestValidateWorkflowTriggersRunAs(t *testing.T) {
var (
req = require.New(t)
soi = sysEvent.SystemOnInterval()
issues = validateWorkflowTriggers(
&types.Workflow{},
&types.Trigger{
Enabled: true,
ResourceType: soi.ResourceType(),
EventType: soi.EventType(),
},
)
)
req.Len(issues, 1)
req.Contains(issues[0].String(), "requires run-as to be set")
}
func TestValidateWorkflowTriggersSubWorkflow(t *testing.T) {
var (
req = require.New(t)
issues = validateWorkflowTriggers(
&types.Workflow{Meta: &types.WorkflowMeta{SubWorkflow: true}},
&types.Trigger{Enabled: true},
)
)
req.Len(issues, 1)
req.Contains(issues[0].String(), "marked as sub-workflow")
}

View File

@@ -0,0 +1,842 @@
package service
import (
"context"
"fmt"
"reflect"
"sync"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
intAuth "github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/label"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/store"
"go.uber.org/zap"
)
type (
workflow struct {
eventbus workflowEventTriggerHandler
store store.Storer
actionlog actionlog.Recorder
ac workflowAccessController
triggers *trigger
session *session
opt options.WorkflowOpt
log *zap.Logger
// cache of workflows, graphs to workflow ID (key, uint64)
cache map[uint64]*wfCacheItem
muxCache *sync.RWMutex
// handle to workflow index
wIndex map[string]uint64
muxWIndex *sync.RWMutex
// workflow function registry
reg *registry
corredorOpt options.CorredorOpt
parser expr.Parsable
}
wfCacheItem struct {
wf *types.Workflow
// caching exec graph
g *wfexec.Graph
// caching user we'll executing workflow with
runAs intAuth.Identifiable
}
workflowAccessController interface {
CanSearchWorkflows(context.Context) bool
CanCreateWorkflow(context.Context) bool
CanReadWorkflow(context.Context, *types.Workflow) bool
CanUpdateWorkflow(context.Context, *types.Workflow) bool
CanDeleteWorkflow(context.Context, *types.Workflow) bool
CanUndeleteWorkflow(context.Context, *types.Workflow) bool
CanManageSessionsOnWorkflow(context.Context, *types.Workflow) bool
Grant(ctx context.Context, rr ...*rbac.Rule) error
workflowExecController
}
workflowExecController interface {
CanExecuteWorkflow(context.Context, *types.Workflow) bool
}
workflowEventTriggerHandler interface {
Register(h eventbus.HandlerFn, ops ...eventbus.HandlerRegOp) uintptr
Unregister(ptrs ...uintptr)
}
workflowUpdateHandler func(ctx context.Context, ns *types.Workflow) (workflowChanges, error)
workflowChanges uint8
workflowInvokerCtxKey struct{}
)
const (
workflowUnchanged workflowChanges = 0
workflowChanged workflowChanges = 1
workflowLabelsChanged workflowChanges = 2
workflowDefChanged workflowChanges = 4
)
func Workflow(log *zap.Logger, corredorOpt options.CorredorOpt, opt options.WorkflowOpt) *workflow {
return &workflow{
log: log,
opt: opt,
actionlog: DefaultActionlog,
store: DefaultStore,
ac: DefaultAccessControl,
triggers: DefaultTrigger,
session: DefaultSession,
eventbus: eventbus.Service(),
cache: make(map[uint64]*wfCacheItem),
wIndex: make(map[string]uint64),
muxCache: &sync.RWMutex{},
muxWIndex: &sync.RWMutex{},
parser: expr.NewParser(),
reg: Registry(),
corredorOpt: corredorOpt,
}
}
func (svc *workflow) Search(ctx context.Context, filter types.WorkflowFilter) (rr types.WorkflowSet, f types.WorkflowFilter, err error) {
var (
wap = &workflowActionProps{filter: &filter}
)
// For each fetched item, store backend will check if it is valid or not
filter.Check = func(res *types.Workflow) (bool, error) {
if !svc.ac.CanReadWorkflow(ctx, res) {
return false, nil
}
return true, nil
}
err = func() (err error) {
if !svc.ac.CanSearchWorkflows(ctx) {
return WorkflowErrNotAllowedToSearch()
}
if len(filter.Labels) > 0 {
filter.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.Workflow{}.LabelResourceKind(),
filter.Labels,
)
if err != nil {
return err
}
// labels specified but no labeled resources found
if len(filter.LabeledIDs) == 0 {
return nil
}
}
if rr, f, err = store.SearchAutomationWorkflows(ctx, svc.store, filter); err != nil {
return err
}
if err = label.Load(ctx, svc.store, toLabeledWorkflows(rr)...); err != nil {
return err
}
return nil
}()
return rr, f, svc.recordAction(ctx, wap, WorkflowActionSearch, err)
}
func (svc *workflow) LookupByID(ctx context.Context, workflowID uint64) (wf *types.Workflow, err error) {
var (
wap = &workflowActionProps{workflow: &types.Workflow{ID: workflowID}}
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) error {
if wf, err = loadWorkflow(ctx, s, workflowID); err != nil {
return err
}
if !svc.ac.CanReadWorkflow(ctx, wf) {
return WorkflowErrNotAllowedToRead()
}
if err = label.Load(ctx, svc.store, wf); err != nil {
return err
}
return nil
})
return wf, svc.recordAction(ctx, wap, WorkflowActionLookup, err)
}
// Create adds new workflow resource and saves it into store
// It updates service's cache
func (svc *workflow) Create(ctx context.Context, new *types.Workflow) (wf *types.Workflow, err error) {
var (
wap = &workflowActionProps{workflow: new}
cUser = intAuth.GetIdentityFromContext(ctx).Identity()
g *wfexec.Graph
runAs intAuth.Identifiable
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if !svc.ac.CanCreateWorkflow(ctx) {
return WorkflowErrNotAllowedToCreate()
}
if new.Meta.Name == "" {
return WorkflowErrMissingName()
}
if !handle.IsValid(new.Handle) {
return WorkflowErrInvalidHandle()
}
if err = svc.uniqueCheck(ctx, new); err != nil {
return err
}
wf = &types.Workflow{
ID: nextID(),
Handle: new.Handle,
Labels: new.Labels,
Meta: new.Meta,
Enabled: new.Enabled,
Trace: new.Trace,
KeepSessions: new.KeepSessions,
Scope: new.Scope,
Steps: new.Steps,
Paths: new.Paths,
// @todo need to check against access control if current user can modify security descriptor
RunAs: new.RunAs,
OwnedBy: cUser,
CreatedAt: *now(),
CreatedBy: cUser,
}
wap.workflow = wf
if g, runAs, err = svc.validateWorkflow(ctx, wf); err != nil {
return
}
svc.updateCache(wf, runAs, g)
if len(wf.Issues) == 0 {
if err = svc.triggers.registerWorkflows(ctx, wf); err != nil {
return err
}
}
if err = store.CreateAutomationWorkflow(ctx, s, wf); err != nil {
return
}
if err = label.Create(ctx, s, wf); err != nil {
return
}
wap.setNew(wf)
return
})
return wf, svc.recordAction(ctx, wap, WorkflowActionCreate, err)
}
// Update modifies existing workflow resource in the store
func (svc *workflow) Update(ctx context.Context, upd *types.Workflow) (*types.Workflow, error) {
return svc.updater(ctx, upd.ID, WorkflowActionUpdate, func(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
if upd.Meta.Name == "" {
return workflowUnchanged, WorkflowErrMissingName()
}
if !svc.ac.CanUpdateWorkflow(ctx, res) {
return workflowUnchanged, WorkflowErrNotAllowedToUpdate()
}
handler := svc.handleUpdate(upd)
return handler(ctx, res)
})
}
func (svc *workflow) DeleteByID(ctx context.Context, workflowID uint64) error {
return trim1st(svc.updater(ctx, workflowID, WorkflowActionDelete, func(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
changes, err := svc.handleDelete(ctx, res)
if err != nil {
return workflowUnchanged, err
}
return changes, err
}))
}
func (svc *workflow) UndeleteByID(ctx context.Context, workflowID uint64) error {
return trim1st(svc.updater(ctx, workflowID, WorkflowActionUndelete, func(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
var (
changes, err = svc.handleUndelete(ctx, res)
)
if err != nil {
return workflowUnchanged, err
}
return changes, err
}))
}
func (svc workflow) uniqueCheck(ctx context.Context, res *types.Workflow) (err error) {
if res.Handle != "" {
if e, _ := store.LookupAutomationWorkflowByHandle(ctx, svc.store, res.Handle); e != nil && e.ID != res.ID {
return WorkflowErrHandleNotUnique()
}
}
return nil
}
func (svc *workflow) updater(ctx context.Context, workflowID uint64, action func(...*workflowActionProps) *workflowAction, fn workflowUpdateHandler) (*types.Workflow, error) {
var (
changes workflowChanges
res *types.Workflow
aProps = &workflowActionProps{workflow: &types.Workflow{ID: workflowID}}
err error
g *wfexec.Graph
runAs intAuth.Identifiable
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
res, err = loadWorkflow(ctx, s, workflowID)
if err != nil {
return
}
if err = label.Load(ctx, svc.store, res); err != nil {
return err
}
aProps.setWorkflow(res)
aProps.setUpdate(res)
if changes, err = fn(ctx, res); err != nil {
return err
}
if g, runAs, err = svc.validateWorkflow(ctx, res); err != nil {
return
}
svc.updateCache(res, runAs, g)
if len(res.Issues) == 0 {
if err = svc.triggers.registerWorkflows(ctx, res); err != nil {
return err
}
}
if changes&workflowChanged > 0 || len(res.Issues) > 0 {
if err = store.UpdateAutomationWorkflow(ctx, svc.store, res); err != nil {
return err
}
}
if changes&workflowLabelsChanged > 0 {
if err = label.Update(ctx, s, res); err != nil {
return
}
}
return
})
return res, svc.recordAction(ctx, aProps, action, err)
}
func (svc *workflow) handleToID(h string) uint64 {
svc.muxWIndex.RLock()
defer svc.muxWIndex.RUnlock()
return svc.wIndex[h]
}
func (svc workflow) handleUpdate(upd *types.Workflow) workflowUpdateHandler {
return func(ctx context.Context, res *types.Workflow) (changes workflowChanges, err error) {
if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) {
return workflowUnchanged, WorkflowErrStaleData()
}
if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) {
return workflowUnchanged, WorkflowErrInvalidHandle()
}
if err := svc.uniqueCheck(ctx, upd); err != nil {
return workflowUnchanged, err
}
if !svc.ac.CanUpdateWorkflow(ctx, res) {
return workflowUnchanged, WorkflowErrNotAllowedToUpdate()
}
if res.Handle != upd.Handle {
changes |= workflowChanged
res.Handle = upd.Handle
}
if res.Enabled != upd.Enabled {
changes |= workflowChanged | workflowDefChanged
res.Enabled = upd.Enabled
}
if upd.Labels != nil {
if label.Changed(res.Labels, upd.Labels) {
changes |= workflowLabelsChanged
res.Labels = upd.Labels
}
}
if res.Trace != upd.Trace {
changes |= workflowChanged | workflowDefChanged
res.Trace = upd.Trace
}
if res.KeepSessions != upd.KeepSessions {
changes |= workflowChanged | workflowDefChanged
res.KeepSessions = upd.KeepSessions
}
if upd.Meta != nil {
if !reflect.DeepEqual(upd.Meta, res.Meta) {
changes |= workflowChanged
res.Meta = upd.Meta
}
}
if upd.Scope != nil {
if !reflect.DeepEqual(upd.Scope, res.Scope) {
changes |= workflowChanged | workflowDefChanged
res.Scope = upd.Scope
}
}
if upd.Steps != nil {
if !reflect.DeepEqual(upd.Steps, res.Steps) {
changes |= workflowChanged | workflowDefChanged
res.Steps = upd.Steps
}
}
if upd.Paths != nil {
if !reflect.DeepEqual(upd.Paths, res.Paths) {
changes |= workflowChanged | workflowDefChanged
res.Paths = upd.Paths
}
}
if res.RunAs != upd.RunAs {
// @todo need to check against access control if current user can modify security descriptor
changes |= workflowChanged | workflowDefChanged
res.RunAs = upd.RunAs
}
if res.OwnedBy != upd.OwnedBy {
// @todo need to check against access control if current user can modify owner
changes |= workflowChanged
res.OwnedBy = upd.OwnedBy
}
if changes&workflowChanged > 0 {
res.UpdatedAt = now()
}
return
}
}
func (svc workflow) handleDelete(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
if !svc.ac.CanDeleteWorkflow(ctx, res) {
return workflowUnchanged, WorkflowErrNotAllowedToDelete()
}
if res.DeletedAt != nil {
// workflow already deleted
return workflowUnchanged, nil
}
res.DeletedAt = now()
return workflowChanged, nil
}
func (svc workflow) handleUndelete(ctx context.Context, res *types.Workflow) (workflowChanges, error) {
if !svc.ac.CanUndeleteWorkflow(ctx, res) {
return workflowUnchanged, WorkflowErrNotAllowedToUndelete()
}
if res.DeletedAt == nil {
// workflow not deleted
return workflowUnchanged, nil
}
res.DeletedAt = nil
return workflowChanged, nil
}
func (svc *workflow) Load(ctx context.Context) error {
var (
set, _, err = store.SearchAutomationWorkflows(ctx, svc.store, types.WorkflowFilter{
Deleted: filter.StateInclusive,
Disabled: filter.StateExcluded,
SubWorkflow: filter.StateInclusive,
})
g *wfexec.Graph
runAs intAuth.Identifiable
)
if err != nil {
return err
}
svc.muxWIndex.Lock()
defer svc.muxWIndex.Unlock()
for _, wf := range set {
svc.wIndex[wf.Handle] = wf.ID
if g, runAs, err = svc.validateWorkflow(ctx, wf); err != nil {
continue
}
svc.updateCache(wf, runAs, g)
}
return svc.triggers.registerWorkflows(ctx, set...)
}
// updateCache
func (svc *workflow) updateCache(wf *types.Workflow, runAs intAuth.Identifiable, g *wfexec.Graph) {
defer svc.muxCache.Unlock()
svc.muxCache.Lock()
if wf.Executable() {
svc.wIndex[wf.Handle] = wf.ID
svc.cache[wf.ID] = &wfCacheItem{g: g, wf: wf, runAs: runAs}
} else {
// remove deleted
delete(svc.cache, wf.ID)
delete(svc.wIndex, wf.Handle)
}
return
}
func (svc *workflow) Exec(ctx context.Context, workflowID uint64, p types.WorkflowExecParams) (*expr.Vars, uint64, types.Stacktrace, error) {
var (
wap = &workflowActionProps{}
t *types.Trigger
results *expr.Vars
wait WaitFn
stacktrace types.Stacktrace
sessionID uint64
)
err := func() (err error) {
svc.muxCache.Lock()
if nil == svc.cache[workflowID] || nil == svc.cache[workflowID].wf {
svc.muxCache.Unlock()
return WorkflowErrNotFound()
}
wf := svc.cache[workflowID].wf
svc.muxCache.Unlock()
wap.setWorkflow(wf)
if !svc.ac.CanExecuteWorkflow(ctx, wf) {
return WorkflowErrNotAllowedToExecute()
}
if !wf.Enabled && !p.Trace {
return WorkflowErrDisabled()
}
// Find the trigger.
// @todo can we cache this as well?
t, err = func() (*types.Trigger, error) {
if p.CallerWorkflowID > 0 {
// skip triggers checking when executed as sub-workflow
// @todo be more strict and allow this ONLY when workflow is flagged as a sub-workflow
return nil, nil
}
var tt types.TriggerSet
// Load triggers directly from the store. At this point we do not care
// about trigger search or read permissions
tt, err = loadWorkflowTriggers(ctx, svc.store, workflowID)
if err != nil {
return nil, err
}
if len(tt) == 0 {
return nil, nil
}
if p.StepID == 0 && len(tt) > 0 {
return tt[0], nil
} else {
for _, tMatch := range tt {
if tMatch.StepID == p.StepID {
return tMatch, nil
}
}
}
if !p.Trace {
// when not doing a trace (designing the workflow)
// we need to be more strict and disallow execution of
// the misconfigured workflows and use of disabled triggers
if t == nil {
return nil, WorkflowErrUnknownWorkflowStep()
} else if !t.Enabled {
return nil, WorkflowErrDisabled()
}
}
if t != nil {
wap.setTrigger(t)
p.StepID = t.StepID
p.EventType = t.EventType
p.ResourceType = t.ResourceType
// merge with input from trigger
// with trigger input vars are overwritten by input vars
p.Input = t.Input.MustMerge(p.Input)
} else {
p.EventType = "onTrace"
}
return nil, nil
}()
if err != nil {
return
}
wait, sessionID, err = svc.exec(ctx, wf, p)
if err != nil {
return err
}
if p.Async && !p.Wait && wf.CheckDeferred() {
// deferred workflow, return right away and keep the workflow session
// running without waiting for the execution
return nil
}
// wait for the workflow to complete
// reuse scope for results
// this will be decoded back to event properties
results, sessionID, _, stacktrace, err = wait(ctx)
return err
}()
return results, sessionID, stacktrace, svc.recordAction(ctx, wap, WorkflowActionExecute, err)
}
// validates workflow by trying to convert it to graph and checking assigned triggers
func (svc *workflow) validateWorkflow(ctx context.Context, wf *types.Workflow) (g *wfexec.Graph, runAs intAuth.Identifiable, err error) {
var (
tt []*types.Trigger
)
g, wf.Issues = Convert(svc, wf)
tt, _, err = store.SearchAutomationTriggers(ctx, svc.store, types.TriggerFilter{
WorkflowID: id.Strings(types.WorkflowSet{wf}.IDs()...),
Deleted: filter.StateExcluded,
Disabled: filter.StateExcluded,
})
if err != nil {
return
}
wf.Issues = append(wf.Issues, validateWorkflowTriggers(wf, tt...)...)
// Returns context with identity set to service user
//
// Current user (identity in the context) might not have
// sufficient privileges to load info about invoker and runner
sysUserCtx := func() context.Context {
return intAuth.SetIdentityToContext(ctx, intAuth.ServiceUser())
}
// @todo this might not be the smartest thing, users might get invalidated after
// we add cache them as workflow runners
if wf.RunAs > 0 {
if runAs, err = DefaultUser.FindByAny(sysUserCtx(), wf.RunAs); err != nil {
wf.Issues = wf.Issues.Append(fmt.Errorf("failed to load run-as user %d: %w", wf.RunAs, err), nil)
} else if !runAs.Valid() {
wf.Issues = wf.Issues.Append(fmt.Errorf("invalid user %d used for workflow run-as", wf.RunAs), nil)
}
}
return
}
func (svc *workflow) exec(ctx context.Context, wf *types.Workflow, p types.WorkflowExecParams) (WaitFn, uint64, error) {
if wf.Issues != nil {
return nil, 0, wf.Issues
}
defer svc.muxCache.Unlock()
svc.muxCache.Lock()
if svc.cache[wf.ID] == nil {
return nil, 0, WorkflowErrInvalidID()
}
var (
g = svc.cache[wf.ID].g
runAs = svc.cache[wf.ID].runAs
scope *expr.Vars
)
// merge workflow scope with the input
scope = wf.Scope.MustMerge(p.Input)
return svc.session.Start(ctx, g, types.SessionStartParams{
Invoker: intAuth.GetIdentityFromContext(ctx),
Runner: runAs,
WorkflowID: wf.ID,
KeepFor: wf.KeepSessions,
Trace: wf.Trace || p.Trace,
Input: scope,
StepID: p.StepID,
EventType: p.EventType,
ResourceType: p.ResourceType,
CallStack: wfexec.GetContextCallStack(ctx),
})
}
func makeWorkflowHandler(svc *workflow, wf *types.Workflow, t *types.Trigger) eventbus.HandlerFn {
return func(ctx context.Context, ev eventbus.Event) (err error) {
var (
scope *expr.Vars
)
if dec, is := ev.(varsEncoder); is {
scope, err = dec.EncodeVars()
if err != nil {
return
}
}
wait, _, err := svc.exec(ctx, wf, types.WorkflowExecParams{
StepID: t.StepID,
EventType: t.EventType,
ResourceType: t.ResourceType,
Input: t.Input.MustMerge(scope),
Trace: wf.Trace,
Async: false,
})
if err != nil {
return
}
if wf.CheckDeferred() {
// deferred workflow, return right away and keep the workflow session
// running without waiting for the execution
return
}
// wait for the workflow to complete
// reuse scope for results
// this will be decoded back to event properties
scope, _, _, _, err = wait(ctx)
if err != nil {
return
}
if dec, is := ev.(varsDecoder); is {
return dec.DecodeVars(scope)
}
return
}
}
func loadWorkflow(ctx context.Context, s store.Storer, workflowID uint64) (res *types.Workflow, err error) {
if workflowID == 0 {
return nil, WorkflowErrInvalidID()
}
if res, err = store.LookupAutomationWorkflowByID(ctx, s, workflowID); errors.IsNotFound(err) {
return nil, WorkflowErrNotFound()
}
return
}
// toLabeledWorkflows converts to []label.LabeledResource
func toLabeledWorkflows(set []*types.Workflow) []label.LabeledResource {
if len(set) == 0 {
return nil
}
ll := make([]label.LabeledResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}
func exprTypeSetter(reg *registry, e *types.Expr) func(string) (expr.Type, error) {
return func(name string) (expr.Type, error) {
if name == "" {
name = "Any"
}
if typ := reg.Type(name); typ != nil {
return typ, nil
} else {
return nil, errors.NotFound(
"unknown or unregistered type %q used for expression %q on %q",
name,
e.Expr,
e.Target,
)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
# List of loggable service actions
resource: automation:workflow
service: workflow
import:
- github.com/cortezaproject/corteza/server/automation/types
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
props:
- name: workflow
type: "*types.Workflow"
fields: [ handle, ID ]
- name: new
type: "*types.Workflow"
fields: [ handle, ID ]
- name: update
type: "*types.Workflow"
fields: [ handle, ID ]
- name: trigger
type: "*types.Trigger"
fields: [ eventType, resourceType, ID, stepID, ]
- name: filter
type: "*types.WorkflowFilter"
actions:
- action: search
log: "searched for matching workflows"
severity: info
- action: lookup
log: "looked-up for a {{workflow}}"
severity: info
- action: create
log: "created {{workflow}}"
- action: update
log: "updated {{workflow}}"
- action: delete
log: "deleted {{workflow}}"
- action: undelete
log: "undeleted {{workflow}}"
- action: execute
# NOTE: only explicitly triggered workflow execution is logged
log: "{{workflow}} executed"
errors:
- error: notFound
message: "workflow not found"
- error: invalidID
message: "invalid ID"
- error: disabled
message: "disabled workflow or trigger"
- error: invalidHandle
message: "invalid handle"
- error: missingName
message: "missing name"
- error: staleData
message: "workflow was modified by someone else or by a workflow after you've opened it. Please refresh to see the latest updated version"
severity: warning
- error: notAllowedToRead
message: "not allowed to read this workflow"
log: "failed to read {{workflow.handle}}; insufficient permissions"
- error: notAllowedToSearch
message: "not allowed to search or list workflows"
log: "failed to search or list workflow; insufficient permissions"
- error: notAllowedToCreate
message: "not allowed to create workflows"
log: "failed to create workflow; insufficient permissions"
- error: notAllowedToUpdate
message: "not allowed to update this workflow"
log: "failed to update {{workflow}}; insufficient permissions"
- error: notAllowedToDelete
message: "not allowed to delete this workflow"
log: "failed to delete {{workflow}}; insufficient permissions"
- error: notAllowedToUndelete
message: "not allowed to undelete this workflow"
log: "failed to undelete {{workflow}}; insufficient permissions"
- error: notAllowedToExecute
message: "not allowed to execute this workflow"
log: "failed to execute {{workflow}}; insufficient permissions"
- error: unknownWorkflowStep
message: "unknown workflow step"
log: "failed to execute {{workflow}}; unknown workflow step"
- error: handleNotUnique
message: "workflow handle not unique"
log: "duplicate handle used for workflow ({{workflow}})"
- error: notAllowedToExecuteCorredorStep
message: "not allowed to run corredorExec function, corredor is disabled"
log: "failed to execute {{workflow}} with corredorExec function step; corredor is disabled"
- error: maximumCallStackSizeExceeded
message: "maximum call stack size exceeded"
log: "maximum call stack size exceeded"

View File

@@ -0,0 +1,943 @@
package service
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/wfexec"
"github.com/cortezaproject/corteza/server/system/automation"
"go.uber.org/zap"
)
type (
wfExecutor interface {
handleToID(string) uint64
Exec(ctx context.Context, workflowID uint64, p types.WorkflowExecParams) (*expr.Vars, uint64, types.Stacktrace, error)
}
workflowConverter struct {
// workflow function registry
reg *registry
parser expr.Parsable
log *zap.Logger
graphs wfExecutor
}
)
func Convert(wfService *workflow, wf *types.Workflow) (*wfexec.Graph, types.WorkflowIssueSet) {
conv := &workflowConverter{
reg: wfService.reg,
parser: wfService.parser,
log: wfService.log,
graphs: wfService,
}
return conv.makeGraph(wf)
}
// Converts workflow definition to wf execution graph
func (svc workflowConverter) makeGraph(def *types.Workflow) (*wfexec.Graph, types.WorkflowIssueSet) {
var (
g = wfexec.NewGraph()
wfii = types.WorkflowIssueSet{}
IDs = make(map[uint64]int)
)
// Basic step verification
for i, s := range def.Steps {
if _, has := IDs[s.ID]; has {
wfii = wfii.Append(
fmt.Errorf("step ID not unique"),
map[string]int{"step": i, "duplicate": IDs[s.ID]},
)
} else {
IDs[s.ID] = i
}
}
// if we have one or more duplicated-id error we need to abort right away
// because all further step positions in issue culprit will be invalid
if len(wfii) > 0 {
return nil, wfii
}
// remove all steps that should not go through
// resolving procedure and should not be added to the graph
ss, _ := def.Steps.Filter(func(step *types.WorkflowStep) (bool, error) {
switch {
case step.Kind == types.WorkflowStepKindVisual:
return false, nil
}
return true, nil
})
for g.Len() < len(ss) {
progress := false
for _, step := range ss {
if g.StepByID(step.ID) != nil {
// resolved
continue
}
// Collect all incoming and outgoing paths
inPaths := make([]*types.WorkflowPath, 0, 8)
outPaths := make([]*types.WorkflowPath, 0, 8)
for _, path := range def.Paths {
if path.ChildID == step.ID {
inPaths = append(inPaths, path)
} else if path.ParentID == step.ID {
outPaths = append(outPaths, path)
}
}
stepIssues := verifyStep(step, inPaths, outPaths)
if resolved, err := svc.workflowStepDefConv(g, def, step, inPaths, outPaths); err != nil {
switch aux := err.(type) {
case types.WorkflowIssueSet:
stepIssues = append(stepIssues, aux...)
continue
case error:
stepIssues = stepIssues.Append(err, nil)
}
} else if resolved {
progress = true
}
wfii = append(wfii, stepIssues.SetCulprit("step", IDs[step.ID])...)
}
if !progress {
// nothing resolved for 1 cycle;
// report every step that is still waiting on a neighbour
//
// steps that are unresolved for their own reasons (unknown
// function, missing argument, ...) already have their issues
// collected above
for _, step := range ss {
if g.StepByID(step.ID) != nil {
continue
}
var (
children []uint64
parents []uint64
)
for _, path := range def.Paths {
if path.ParentID == step.ID && g.StepByID(path.ChildID) == nil {
children = append(children, path.ChildID)
}
if path.ChildID == step.ID && g.StepByID(path.ParentID) == nil {
parents = append(parents, path.ParentID)
}
}
if len(children) == 0 && len(parents) == 0 {
continue
}
wfii = wfii.Append(fmt.Errorf(
"failed to resolve dependencies for %s step %d, waiting for unresolved parent steps %v and child steps %v",
step.Kind,
step.ID,
parents,
children,
), map[string]int{"step": IDs[step.ID]})
}
if len(wfii) == 0 {
// resolving stalled but nothing explains why
wfii = wfii.Append(fmt.Errorf("failed to resolve workflow step dependencies"), map[string]int{})
}
break
}
}
// add parents that were still unresolved when a join gateway was created
for _, step := range ss {
if step.Kind != types.WorkflowStepKindGateway || step.Ref != "join" {
continue
}
resolved, is := g.StepByID(step.ID).(interface{ AddPath(wfexec.Step) })
if !is {
continue
}
for _, path := range def.Paths {
if path.ChildID != step.ID {
continue
}
if parent := g.StepByID(path.ParentID); parent != nil {
resolved.AddPath(parent)
}
}
}
for pos, path := range def.Paths {
if g.StepByID(path.ChildID) == nil {
wfii = wfii.Append(fmt.Errorf("failed to resolve step with ID %d", path.ChildID), map[string]int{"path": pos})
continue
}
if g.StepByID(path.ParentID) == nil {
wfii = wfii.Append(fmt.Errorf("failed to resolve step with ID %d", path.ParentID), map[string]int{"path": pos})
continue
}
if len(wfii) > 0 {
// pointless to fill the if there are errors
continue
}
g.AddParent(
g.StepByID(path.ChildID),
g.StepByID(path.ParentID),
)
}
if len(wfii) > 0 {
return nil, wfii.Distinct()
}
return g, nil
}
// converts all step definitions into workflow.Step instances
//
// if this func returns nil for step and error, assume unresolved dependencies
func (svc workflowConverter) workflowStepDefConv(g *wfexec.Graph, def *types.Workflow, s *types.WorkflowStep, in, out []*types.WorkflowPath) (bool, error) {
if err := svc.parseExpressions(s.Arguments...); err != nil {
return false, errors.Internal("failed to parse step arguments expressions for %s: %s", s.Kind, err).Wrap(err)
}
if err := svc.parseExpressions(s.Results...); err != nil {
return false, errors.Internal("failed to parse step results expressions for %s: %s", s.Kind, err).Wrap(err)
}
conv, err := func() (wfexec.Step, error) {
switch s.Kind {
case types.WorkflowStepKindVisual:
return nil, nil
case types.WorkflowStepKindDebug:
return svc.convDebugStep(s)
case types.WorkflowStepKindExpressions:
return svc.convExpressionStep(s)
case types.WorkflowStepKindGateway:
return svc.convGateway(g, s, in, out)
case types.WorkflowStepKindFunction, types.WorkflowStepKindIterator:
return svc.convFunctionStep(g, s, out)
case types.WorkflowStepKindError:
return svc.convErrorStep(s)
case types.WorkflowStepKindTermination:
return svc.convTerminationStep()
case types.WorkflowStepKindPrompt:
return svc.convPromptStep(s)
case types.WorkflowStepKindDelay:
return svc.convDelayStep(s)
case types.WorkflowStepKindErrHandler:
return svc.convErrorHandlerStep(g, s, out)
case types.WorkflowStepKindBreak:
return svc.convBreakStep()
case types.WorkflowStepKindContinue:
return svc.convContinueStep()
case types.WorkflowStepKindExecWorkflow:
return svc.convExecWorkflowStep(def, s)
default:
return nil, errors.Internal("unsupported step kind %q", s.Kind)
}
}()
if err != nil {
return false, err
} else if conv != nil {
conv.SetID(s.ID)
g.AddStep(conv)
return true, err
} else {
// signal caller that we were unable to
// resolve definition at the moment
return false, nil
}
}
func (svc workflowConverter) convGateway(g *wfexec.Graph, s *types.WorkflowStep, in, out []*types.WorkflowPath) (wfexec.Step, error) {
switch s.Ref {
case "fork":
return wfexec.ForkGateway(), nil
case "join":
var (
ss []wfexec.Step
)
// Collect parents that are already in the graph; the rest are added
// once the whole graph is resolved.
//
// Waiting for all of them here deadlocks on an iterator with its exit
// path leading to this gateway: the iterator can not be resolved
// before the gateway is in the graph.
for _, p := range in {
if parent := g.StepByID(p.ParentID); parent != nil {
ss = append(ss, parent)
}
}
return wfexec.JoinGateway(ss...), nil
case "incl", "excl":
var (
pp []*wfexec.GatewayPath
)
for _, c := range out {
child := g.StepByID(c.ChildID)
if child == nil {
return nil, nil
}
if len(c.Expr) > 0 {
if err := svc.parser.ParseEvaluators(c); err != nil {
return nil, err
}
}
// wrapping with fn to make sure that we're dealing with the right wf path inside gw-path tester fn
err := func(c types.WorkflowPath) error {
p, err := wfexec.NewGatewayPath(child, func(ctx context.Context, scope *expr.Vars) (bool, error) {
if len(c.Expr) == 0 {
return true, nil
}
return c.Test(ctx, scope)
})
if err != nil {
return err
}
pp = append(pp, p)
return nil
}(*c)
if err != nil {
return nil, err
}
}
if s.Ref == "excl" {
return wfexec.ExclGateway(pp...)
} else {
return wfexec.InclGateway(pp...)
}
}
return nil, errors.Internal("unexpected workflow configuration")
}
func (svc workflowConverter) convErrorHandlerStep(g *wfexec.Graph, s *types.WorkflowStep, out []*types.WorkflowPath) (wfexec.Step, error) {
var (
results = &expr.Vars{}
)
// evaluate results and set target path for each expression
for _, r := range s.Results {
err := results.Set(r.Expr, r.Target)
if err != nil {
return nil, err
}
}
switch len(out) {
case 0:
return nil, fmt.Errorf("expecting at least one path out of error handling step")
case 1:
// remove error handler
return types.ErrorHandlerStep(nil, results), nil
case 2:
errorHandler := g.StepByID(out[1].ChildID)
if errorHandler == nil {
// wait for it to be resolved
return nil, nil
}
return types.ErrorHandlerStep(errorHandler, results), nil
default:
// this might be extended in the future to allow different paths using expression
// but then again, this can be solved by gateway path following the error handling step
return nil, fmt.Errorf("max 2 paths out of error handling step")
}
}
func (svc workflowConverter) convExpressionStep(s *types.WorkflowStep) (wfexec.Step, error) {
return types.ExpressionsStep(s.Arguments...), nil
}
// internal debug step that can log entire
func (svc workflowConverter) convDebugStep(_ *types.WorkflowStep) (wfexec.Step, error) {
return types.DebugStep(svc.log), nil
}
func (svc workflowConverter) convFunctionStep(g *wfexec.Graph, s *types.WorkflowStep, out []*types.WorkflowPath) (wfexec.Step, error) {
reg := Registry()
if def := reg.Function(s.Ref); def == nil {
return nil, errors.Internal("unknown function %q", s.Ref)
} else {
if def.Kind != string(s.Kind) {
return nil, fmt.Errorf("unexpected %s on %s step", def.Kind, s.Kind)
}
var (
err error
isIterator = def.Kind == types.FunctionKindIterator
)
if isIterator {
if def.Iterator == nil {
return nil, errors.Internal("iterator handler for %q not set", s.Ref)
}
} else {
if def.Handler == nil {
return nil, errors.Internal("function handler for %q not set", s.Ref)
}
}
if err = def.Parameters.VerifyArguments(s.Arguments); err != nil {
return nil, errors.Internal("failed to verify argument expressions for %s %s: %s", s.Kind, s.Ref, err).Wrap(err)
}
if err = def.Results.VerifyResults(s.Results); err != nil {
return nil, errors.Internal("failed to verify result expressions for %s %s: %s", s.Kind, s.Ref, err).Wrap(err)
}
if isIterator {
if len(out) != 2 {
return nil, fmt.Errorf("expecting exactly 2 outbound paths for iterator")
}
var (
next = g.StepByID(out[0].ChildID)
exit = g.StepByID(out[1].ChildID)
)
if next == nil || exit == nil {
// wait for steps to be resolved
return nil, nil
}
return types.IteratorStep(def, s.Arguments, s.Results, next, exit)
} else {
return types.FunctionStep(def, s.Arguments, s.Results)
}
}
}
// creates error step
//
// Expects ZERO outgoing paths and
func (svc workflowConverter) convErrorStep(s *types.WorkflowStep) (wfexec.Step, error) {
const (
argName = "message"
)
var (
args = types.ExprSet(s.Arguments)
)
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
var (
msg string
result, err = args.Eval(ctx, r.Scope)
)
if err != nil {
return nil, err
}
if result.Has(argName) {
str, _ := expr.NewString(expr.Must(result.Select(argName)))
msg = str.GetValue()
} else {
if aux, is := args.GetByTarget(argName).Value.(string); is {
msg = aux
} else {
msg = "ERROR"
}
}
return nil, errors.Automation("%s", msg)
}), nil
}
// converts termination definition to wfexec.Step
func (svc workflowConverter) convTerminationStep() (wfexec.Step, error) {
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
return wfexec.Termination(), nil
}), nil
}
// converts prompt definition to wfexec.Step
//
// If input is not passed (session was not resumed from suspension after prompt), we send wait-for-input
// result that signals session it needs to suspend and wait for the input.
// At this point, we take the current scope run it through step arguments and store the output with the suspended state
//
// After session is resumed, input should be set (not nil) and evaluated through results.
func (svc workflowConverter) convPromptStep(s *types.WorkflowStep) (wfexec.Step, error) {
// Use expression step as base for prompt step
var (
args = types.ExprSet(s.Arguments)
res = types.ExprSet(s.Results)
)
const (
ownerArgName = "owner"
)
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
if r.Input == nil {
// input is only set (not nil) when session is resumed on prompt step
// suspend the session and wait for input
// @todo take scope, eval with arguments and add it to suspended state
payload, err := args.Eval(ctx, r.Scope)
if err != nil {
return nil, err
}
var ownerId uint64 = 0
if payload.Has(ownerArgName) {
// @note for now only types.User and uint64 are supported.
// At some point we should extend this to handles, emails, usernames, ...
// but for now this should be fine
// @todo should we validate existence here?
// I'm not sure how to bring in a thing to validate it so I omitted it for now.
param := expr.Must(payload.Select(ownerArgName))
switch casted := param.(type) {
case *automation.User:
ownerId = casted.GetValue().ID
case *expr.ID:
ownerId = casted.GetValue()
default:
return nil, fmt.Errorf("cannot select owner: unsupported type provided %T", casted)
}
} else {
if i := auth.GetIdentityFromContextWithKey(ctx, workflowInvokerCtxKey{}); i != nil {
ownerId = i.Identity()
}
}
return wfexec.Prompt(ownerId, s.Ref, payload), nil
}
results, err := res.Eval(ctx, r.Scope.MustMerge(r.Input))
if err != nil {
return nil, err
}
return results, nil
}), nil
}
// converts delay definition to wfexec.Step
func (svc workflowConverter) convDelayStep(s *types.WorkflowStep) (wfexec.Step, error) {
return types.DelayStep(s.Arguments), nil
}
func (svc workflowConverter) convBreakStep() (wfexec.Step, error) {
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
return wfexec.LoopBreak(), nil
}), nil
}
func (svc workflowConverter) convContinueStep() (wfexec.Step, error) {
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (wfexec.ExecResponse, error) {
return wfexec.LoopContinue(), nil
}), nil
}
// creates workflow-executor step
//
// Expects max ONE outgoing paths and ONE incoming path
func (svc workflowConverter) convExecWorkflowStep(wf *types.Workflow, s *types.WorkflowStep) (_ wfexec.Step, err error) {
const (
// sub-workflow's scope
// expecting expr.Vars
argNameScope = "scope"
// workflow id or handle
argNameWorkflow = "workflow"
)
return wfexec.NewGenericStep(func(ctx context.Context, r *wfexec.ExecRequest) (resp wfexec.ExecResponse, err error) {
var (
p = types.WorkflowExecParams{
// sub-workflows should not be executed asynchronously
// might lead to uncontrolled resource consumption
Async: false,
// @todo figure out how check if trace is enabled
Trace: false,
// always wait for the sub-workflow even when deferred
Wait: true,
Input: expr.EmptyVars(),
// who's calling sub-workflow?
CallerSessionID: r.SessionID,
CallerStepID: s.ID,
CallerWorkflowID: wf.ID,
}
result *expr.Vars
workflowID uint64
workflowHandle string
)
if ap, err := processArguments(ctx, s.Arguments, r.Scope); err != nil {
return nil, err
} else {
if ap.string(argNameWorkflow, &workflowHandle) {
if workflowID = svc.graphs.handleToID(workflowHandle); workflowID == 0 {
return nil, errors.NotFound("workflow with handle %s not found", workflowHandle)
}
} else if !ap.uint64(argNameWorkflow, &workflowID) {
return nil, errors.InvalidData("workflow ID must be provided")
}
// let's make sure we're not calling ourselves
if p.CallerWorkflowID == workflowID {
return nil, errors.InvalidData("recursive workflow call is not allowed")
}
// scope for the sub-workflow
ap.vars(argNameScope, p.Input)
}
result, _, _, err = svc.graphs.Exec(ctx, workflowID, p)
if err != nil {
return
}
result, err = types.ExprSet(s.Results).Eval(ctx, result)
if err != nil {
return
}
return result, nil
}), nil
}
func (svc workflowConverter) parseExpressions(ee ...*types.Expr) (err error) {
for _, e := range ee {
if len(strings.TrimSpace(e.Expr)) > 0 {
if err = svc.parser.ParseEvaluators(e); err != nil {
return
}
}
if err = e.SetType(exprTypeSetter(svc.reg, e)); err != nil {
return err
}
for _, t := range e.Tests {
if err = svc.parser.ParseEvaluators(t); err != nil {
return
}
}
}
return nil
}
func verifyStep(s *types.WorkflowStep, in, out types.WorkflowPathSet) types.WorkflowIssueSet {
const (
arguments = "argument"
results = "result"
outbound = "outbound path"
inbound = "inbound path"
)
var (
ii = types.WorkflowIssueSet{}
// checks if count of arguments, results, outbound or inbound paths is between (incl) min and max
count = func(min, max int, typ string) func() error {
return func() error {
var (
l int
)
switch typ {
case arguments:
l = len(s.Arguments)
case results:
l = len(s.Results)
case outbound:
l = len(out)
case inbound:
l = len(in)
}
switch {
case max == 0 && min == max && l != min:
return errors.Internal("%s step does not expect any %ss", s.Kind, typ)
case max > 0 && min == max && l != min:
return errors.Internal("%s step expects exactly %d %s(s)", s.Kind, min, typ)
case l < min:
return errors.Internal("%s step expects at least %d %s(s)", s.Kind, min, typ)
case max > 0 && l > max:
return errors.Internal("%s step expects no more than %d %s(s)", s.Kind, max, typ)
}
return nil
}
}
// check if reference is set on the step
requiredRef = func() error {
if s.Ref == "" {
return errors.Internal("%s step expects reference", s.Kind)
}
return nil
}
// check if reference is set on the step
numericRef = func() error {
// @todo SUBWF enable this back
// if 0 == cast.ToUint64(s.Ref) {
// return errors.Internal("%s step expects workflow reference ID", s.Kind)
// }
return nil
}
// reference should not be set on the step
noRef = func() error {
if s.Ref != "" {
return errors.Internal("%s step does not expects reference", s.Kind)
}
return nil
}
// check for corredor function step(s) are allowed or not
checkDisabledFunc = func() error {
if !DefaultWorkflow.corredorOpt.Enabled && s.Ref == "corredorExec" {
return WorkflowErrNotAllowedToExecuteCorredorStep()
}
return nil
}
// checks if argument is present
checkArg = func(argName string, tt ...expr.Type) func() error {
return func() error {
msgArg := types.ExprSet(s.Arguments).GetByTarget(argName)
if msgArg == nil {
return nil
}
for _, typ := range tt {
if msgArg.Type == typ.Type() {
return nil
}
}
return errors.Internal("unexpected type %q for argument %q on step type %q", msgArg.Type, argName, s.Kind)
}
}
// checks if argument is present
requiredArg = func(argName string, tt ...expr.Type) func() error {
return func() error {
if msgArg := types.ExprSet(s.Arguments).GetByTarget(argName); msgArg == nil {
return errors.Internal("%s step expects to have '%s' argument", s.Kind, argName)
}
return checkArg(argName, tt...)()
}
}
// should not have any arguments, results, outbound or inbound paths
zero = func(typ string) func() error { return count(0, 0, typ) }
// step should not have any outbound paths
last = func() error { return count(0, 0, outbound)() }
// gw sanity check
gatewayCheck = func(checks ...func() error) []func() error {
switch s.Ref {
case "join":
return append(checks, count(1, -1, inbound))
case "fork", "incl", "excl":
return append(checks, count(1, -1, outbound))
}
return append(checks, func() error { return fmt.Errorf("unknown gateway type") })
}
checks = make([]func() error, 0)
)
switch s.Kind {
case types.WorkflowStepKindErrHandler:
checks = append(checks,
noRef,
zero(arguments),
count(1, 2, outbound),
)
case types.WorkflowStepKindDebug:
checks = append(checks,
noRef,
zero(results),
count(0, 1, outbound),
)
case types.WorkflowStepKindVisual:
checks = append(checks,
zero(results),
zero(arguments),
)
case types.WorkflowStepKindExpressions:
checks = append(checks,
noRef,
zero(results),
count(1, -1, arguments),
count(0, 1, outbound),
)
case types.WorkflowStepKindGateway:
checks = append(checks, gatewayCheck(zero(arguments), zero(results))...)
case types.WorkflowStepKindError:
checks = append(checks,
requiredArg("message", expr.String{}),
count(0, 1, arguments),
zero(results),
last,
)
case types.WorkflowStepKindTermination:
checks = append(checks,
noRef,
zero(arguments),
zero(results),
last,
)
case types.WorkflowStepKindFunction:
checks = append(checks,
requiredRef,
numericRef,
checkDisabledFunc,
count(0, 1, outbound),
)
case types.WorkflowStepKindIterator:
checks = append(checks,
requiredRef,
count(2, 2, outbound),
)
case types.WorkflowStepKindPrompt:
checks = append(checks,
requiredRef,
count(0, 1, outbound),
)
case types.WorkflowStepKindDelay:
checks = append(checks,
checkArg("timestamp", expr.DateTime{}),
checkArg("offset", expr.Duration{}),
count(1, 1, arguments),
zero(results),
count(0, 1, outbound),
)
case types.WorkflowStepKindBreak:
checks = append(checks,
noRef,
zero(arguments),
zero(results),
count(0, 1, outbound),
last,
)
case types.WorkflowStepKindContinue:
checks = append(checks,
noRef,
zero(arguments),
zero(results),
count(0, 1, outbound),
last,
)
case types.WorkflowStepKindExecWorkflow:
checks = append(checks,
noRef,
// required "workflow" with handle or ID
requiredArg("workflow", expr.Handle{}, expr.ID{}),
// optional "scope" as expr.Vars
checkArg("scope", expr.Vars{}),
// expecting at least workflow + optional scope, nothing more
count(1, 2, arguments),
count(0, 1, outbound),
)
case "":
return ii.Append(fmt.Errorf("missing step kind"), nil)
default:
return ii.Append(fmt.Errorf("unknown step kind '%s'", s.Kind), nil)
}
for _, check := range checks {
if err := check(); err != nil {
ii = ii.Append(err, nil)
}
}
return ii
}

View File

@@ -0,0 +1,75 @@
package service
import (
"context"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
)
type (
// argsProc is a helper to process arguments
//
// when initialised it evaluates expressions using given scope
// it provides methods to extract values from the result
argsProc struct {
expr types.ExprSet
result *expr.Vars
}
)
func processArguments(ctx context.Context, expr []*types.Expr, scope *expr.Vars) (p *argsProc, err error) {
p = &argsProc{expr: expr}
if p.result, err = p.expr.Eval(ctx, scope); err != nil {
return nil, err
}
return
}
func (p *argsProc) bool(name string, val *bool) bool { return getStepArg(p, name, val) }
func (p *argsProc) string(name string, val *string) bool { return getStepArg(p, name, val) }
func (p *argsProc) uint64(name string, val *uint64) bool { return getStepArg(p, name, val) }
// vars handles assigning of expr.Vars
//
// not as straightforward as plain types
func (p *argsProc) vars(name string, val *expr.Vars) bool {
// make an auxiliary variable to hold the results q
aux := make(map[string]expr.TypedValue)
// extract it
if !getStepArg(p, name, &aux) {
return false
}
if val == nil {
panic("initialize Vars before calling the function")
}
vars, _ := expr.NewVars(aux)
*val = *vars
return true
}
// getStepArg is a helper to extract step argument from result
func getStepArg[T any](p *argsProc, arg string, val *T) bool {
var (
aux any
)
if p.result.Has(arg) {
aux = expr.Must(p.result.Select(arg)).Get()
} else if exp := p.expr.GetByTarget(arg); exp != nil {
aux = p.expr.GetByTarget(arg).Value.(T)
} else {
return false
}
if conv, is := aux.(T); is {
*val = conv
return true
}
return false
}

View File

@@ -0,0 +1,83 @@
package service
import (
"context"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/stretchr/testify/require"
"testing"
)
func TestProcessArguments(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var (
req = require.New(t)
p, err = processArguments(context.Background(), []*types.Expr{}, nil)
tBool bool
tStr string
tUint uint64
tVars *expr.Vars
)
req.NoError(err)
req.NotNil(p)
req.False(p.bool("not-there", &tBool))
req.False(tBool)
req.False(p.string("not-there", &tStr))
req.Empty(tStr)
req.False(p.uint64("not-there", &tUint))
req.Zero(tUint)
req.False(p.vars("not-there", tVars))
req.Zero(tVars)
})
t.Run("expr", func(t *testing.T) {
var (
req = require.New(t)
ee = []*types.Expr{
types.NewTypedExpr("tBool", "true", &expr.Boolean{}),
types.NewTypedExpr("tString", "\"foo\"", &expr.String{}),
types.NewTypedExpr("tUint", "42", &expr.UnsignedInteger{}),
types.NewTypedExpr("tVars", "{}", &expr.Vars{}),
types.NewTypedExpr("tVars.foo", "\"bar\"", &expr.String{}),
}
)
req.NoError(expr.NewGvalParser().ParseEvaluators(func() []expr.Evaluator {
oo := make([]expr.Evaluator, len(ee))
for i, e := range ee {
oo[i] = e
}
return oo
}()...))
var (
p, err = processArguments(context.Background(), ee, nil)
tBool bool
tStr string
tUint uint64
tVars = &expr.Vars{}
)
req.NoError(err)
req.NotNil(p)
req.True(p.bool("tBool", &tBool))
req.True(tBool)
req.True(p.string("tString", &tStr))
req.Equal("foo", tStr)
req.True(p.uint64("tUint", &tUint))
req.Equal(uint64(42), tUint)
req.True(p.vars("tVars", tVars))
req.Equal(expr.Must(expr.NewVars(map[string]any{"foo": "bar"})), tVars)
})
}