chg: [worflow] Started removing feature from initial design

- Multiple workflows per trigger
- Custom Workflow per user
- Workflow import/export
- Blocking & Parallel path from triggers
pull/8530/head
Sami Mokaddem 2022-06-07 13:46:26 +02:00
parent 52e0a059f1
commit d180c2cc17
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
17 changed files with 355 additions and 307 deletions

View File

@ -24,7 +24,6 @@ class WorkflowsController extends AppController
$params = [
'filters' => ['name', 'uuid'],
'quickFilters' => ['name', 'uuid'],
'contain' => ['Organisation']
];
$this->CRUD->index($params);
if ($this->IndexFilter->isRest()) {
@ -35,19 +34,16 @@ class WorkflowsController extends AppController
public function rebuildRedis()
{
$this->Workflow->rebuildRedis($this->Auth->user());
$this->Workflow->rebuildRedis();
}
public function add()
{
$currentUser = $this->Auth->user();
$params = [
'beforeSave' => function ($data) use ($currentUser) {
'beforeSave' => function ($data) {
if (empty($data['Workflow']['uuid'])) {
$data['Workflow']['uuid'] = CakeText::uuid();
}
$data['Workflow']['user_id'] = $currentUser['id'];
$data['Workflow']['org_id'] = $currentUser['org_id'];
if (!isset($data['Workflow']['description'])) {
$data['Workflow']['description'] = '';
}
@ -72,18 +68,18 @@ class WorkflowsController extends AppController
public function edit($id)
{
$this->set('id', $id);
$savedWorkflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id);
$savedWorkflow = $this->Workflow->fetchWorkflow($id);
if ($this->request->is('post') || $this->request->is('put')) {
$newWorkflow = $this->request->data;
$newWorkflow['Workflow']['data'] = JsonTool::decode($newWorkflow['Workflow']['data']);
$newWorkflow = $this->__applyDataFromSavedWorkflow($newWorkflow, $savedWorkflow);
$errors = $this->Workflow->editWorkflow($this->Auth->user(), $newWorkflow);
$errors = $this->Workflow->editWorkflow($newWorkflow);
$redirectTarget = ['action' => 'view', $id];
if (!empty($errors)) {
return $this->__getFailResponseBasedOnContext($errors, null, 'edit', $this->Workflow->id, $redirectTarget);
} else {
$successMessage = __('Workflow saved.');
$savedWorkflow =$this->Workflow->fetchWorkflow($this->Auth->user(), $id);
$savedWorkflow =$this->Workflow->fetchWorkflow($id);
return $this->__getSuccessResponseBasedOnContext($successMessage, $savedWorkflow, 'edit', false, $redirectTarget);
}
} else {
@ -98,7 +94,6 @@ class WorkflowsController extends AppController
public function delete($id)
{
$params = [
'conditions' => $this->Workflow->buildACLConditions($this->Auth->user()),
];
$this->CRUD->delete($id, $params);
if ($this->IndexFilter->isRest()) {
@ -109,8 +104,6 @@ class WorkflowsController extends AppController
public function view($id)
{
$this->CRUD->view($id, [
'conditions' => $this->Workflow->buildACLConditions($this->Auth->user()),
'contain' => ['Organisation', 'User']
]);
if ($this->IndexFilter->isRest()) {
return $this->restResponsePayload;
@ -121,59 +114,90 @@ class WorkflowsController extends AppController
public function enable($id)
{
$errors = $this->Workflow->toggleWorkflow($this->Auth->user(), $id, true);
$errors = $this->Workflow->toggleWorkflow($id, true);
$redirectTarget = ['action' => 'index'];
if (!empty($errors)) {
return $this->__getFailResponseBasedOnContext($errors, null, 'edit', $this->Workflow->id, $redirectTarget);
} else {
$successMessage = __('Workflow enabled.');
$savedWorkflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id);
$savedWorkflow = $this->Workflow->fetchWorkflow($id);
return $this->__getSuccessResponseBasedOnContext($successMessage, $savedWorkflow, 'edit', false, $redirectTarget);
}
}
public function disable($id)
{
$errors = $this->Workflow->toggleWorkflow($this->Auth->user(), $id, false);
$errors = $this->Workflow->toggleWorkflow($id, false);
$redirectTarget = ['action' => 'index'];
if (!empty($errors)) {
return $this->__getFailResponseBasedOnContext($errors, null, 'edit', $this->Workflow->id, $redirectTarget);
} else {
$successMessage = __('Workflow disabled.');
$savedWorkflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id);
$savedWorkflow = $this->Workflow->fetchWorkflow($id);
return $this->__getSuccessResponseBasedOnContext($successMessage, $savedWorkflow, 'edit', false, $redirectTarget);
}
}
public function editor($id = false)
public function editor($trigger_id)
{
$modules = $this->Workflow->getModulesByType();
$workflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id);
$modules = $this->Workflow->attachNotificationToModules($this->Auth->user(), $modules, $workflow);
$workflows = $this->Workflow->fetchWorkflows($this->Auth->user());
$trigger_ids = Hash::extract($modules['blocks_trigger'], '{n}.id');
if (!in_array($trigger_id, $trigger_ids)) {
return $this->__getFailResponseBasedOnContext(
[__('Unkown trigger %s', $trigger_id)],
null,
'add',
$trigger_id,
['controller' => 'workflows', 'action' => 'triggers']
);
}
$workflow = $this->Workflow->fetchWorkflowByTrigger($trigger_id, false);
if (empty($workflow)) { // Workflow do not exists yet. Create it.
$this->Workflow->create();
$savedWorkflow = $this->Workflow->save([
'name' => sprintf('Workflow for trigger %s', $trigger_id),
'trigger_id' => $trigger_id,
]);
if (empty($savedWorkflow)) {
return $this->__getFailResponseBasedOnContext(
[__('Could not create workflow for trigger %s', $trigger_id), $this->validationErrors],
null,
'add',
$trigger_id,
['controller' => 'workflows', 'action' => 'editor']
);
}
$workflow = $savedWorkflow;
}
$modules = $this->Workflow->attachNotificationToModules($modules, $workflow);
$this->set('selectedWorkflow', $workflow);
$this->set('workflows', $workflows);
$this->set('modules', $modules);
}
public function triggers()
{
$triggers = $this->Workflow->getModulesByType('trigger');
$triggers = $this->Workflow->attachWorkflowToTriggers($triggers);
$data = $triggers;
if ($this->_isRest()) {
return $this->RestResponse->viewData($data, $this->response->type());
}
$this->set('data', $data);
$this->set('menuData', ['menuList' => 'workflows', 'menuItem' => 'index_trigger']);
}
public function moduleIndex()
{
$modules = $this->Workflow->getModulesByType();
$this->Module = ClassRegistry::init('Module');
$mispModules = $this->Module->getModules('Enrichment');
$mispModules = $this->Module->getModules('Action');
$this->set('module_service_error', !is_array($mispModules));
// FIXME: Apply ACL to filter out module not available to users
$filters = $this->IndexFilter->harvestParameters(['type']);
$moduleType = $filters['type'] ?? 'trigger';
if ($moduleType == 'trigger') {
$triggers = $modules['blocks_trigger'];
$triggers = $this->Workflow->attachWorkflowsToTriggers($this->Auth->user(), $triggers, true);
$data = $triggers;
} elseif ($moduleType == 'all') {
$moduleType = $filters['type'] ?? 'action';
if ($moduleType == 'all') {
$data = array_merge(
$modules["blocks_trigger"],
$modules["blocks_logic"],
$modules["blocks_action"]
$modules["blocks_action"],
$modules["blocks_logic"]
);
} else {
$data = $modules["blocks_{$moduleType}"];
@ -192,8 +216,9 @@ class WorkflowsController extends AppController
if (empty($module)) {
throw new NotFoundException(__('Invalid trigger ID'));
}
if ($module['module_type'] == 'trigger') {
$module = $this->Workflow->attachWorkflowsToTriggers($this->Auth->user(), [$module], true)[0];
$is_trigger = $module['module_type'] == 'trigger';
if ($is_trigger) {
$module = $this->Workflow->attachWorkflowToTriggers([$module])[0];
}
if ($this->_isRest()) {
return $this->RestResponse->viewData($module, $this->response->type());
@ -220,7 +245,7 @@ class WorkflowsController extends AppController
public function export($id)
{
$workflow = $this->Workflow->fetchWorkflow($this->Auth->user(), $id);
$workflow = $this->Workflow->fetchWorkflow($id);
$content = JsonTool::encode($workflow, JSON_PRETTY_PRINT);
$this->response->body($content);
$this->response->type('json');
@ -234,7 +259,7 @@ class WorkflowsController extends AppController
if (empty($trigger)) {
throw new NotFoundException(__('Invalid trigger ID'));
}
$trigger = $this->Workflow->attachWorkflowsToTriggers($this->Auth->user(), [$trigger], true)[0];
$trigger = $this->Workflow->attachWorkflowToTriggers([$trigger])[0];
$workflow_order = [];
if (!empty($trigger['Workflows']['blocking'])) {
$workflow_order = Hash::extract($trigger['Workflows']['blocking'], '{n}.Workflow.id');
@ -291,7 +316,7 @@ class WorkflowsController extends AppController
return $this->RestResponse->saveFailResponse('Workflow', $action, $id, $message, false, $data);
} else {
$this->Flash->error($message);
$this->redirect($this->referer());
$this->redirect($redirect);
}
}
@ -300,7 +325,7 @@ class WorkflowsController extends AppController
if (!isset($newReport['Workflow'])) {
$newReport = ['Workflow' => $newWorkflow];
}
$ignoreFieldList = ['id', 'uuid', 'org_id', 'user_id'];
$ignoreFieldList = ['id', 'uuid'];
foreach (Workflow::CAPTURE_FIELDS as $field) {
if (!in_array($field, $ignoreFieldList) && isset($newWorkflow['Workflow'][$field])) {
$savedWorkflow['Workflow'][$field] = $newWorkflow['Workflow'][$field];

View File

@ -15,14 +15,6 @@ class Workflow extends AppModel
];
public $belongsTo = [
'User' => [
'className' => 'User',
'foreignKey' => 'user_id',
],
'Organisation' => [
'className' => 'Organisation',
'foreignKey' => 'org_id'
]
];
public $validate = [
@ -55,8 +47,6 @@ class Workflow extends AppModel
];
public $defaultContain = [
// 'Organisation',
// 'User'
];
private $loaded_modules = [];
@ -81,6 +71,11 @@ class Workflow extends AppModel
public function beforeValidate($options = array())
{
parent::beforeValidate();
if (empty($this->data['Workflow']['uuid'])) {
$this->data['Workflow']['uuid'] = CakeText::uuid();
} else {
$this->data['Workflow']['uuid'] = strtolower($this->data['Workflow']['uuid']);
}
if (empty($this->data['Workflow']['data'])) {
$this->data['Workflow']['data'] = [];
}
@ -147,10 +142,10 @@ class Workflow extends AppModel
return !empty($this->__getWorkflowsIDPerTrigger($trigger_id));
}
public function rebuildRedis($user)
public function rebuildRedis()
{
$redis = $this->setupRedisWithException();
$workflows = $this->fetchWorkflows($user);
$workflows = $this->fetchWorkflows();
$keys = $redis->keys(Workflow::REDIS_KEY_WORKFLOW_NAMESPACE . ':*');
$redis->delete($keys);
foreach ($workflows as $wokflow) {
@ -285,79 +280,60 @@ class Workflow extends AppModel
}
/**
* buildACLConditions Generate ACL conditions for viewing the workflow
* attachWorkflowToTriggers Collect the workflows listening to this trigger
*
* @param array $user
* @return array
*/
public function buildACLConditions(array $user)
{
$conditions = [];
if (!$user['Role']['perm_site_admin']) {
$conditions['Workflow.org_id'] = $user['org_id'];
}
return $conditions;
}
public function canEdit(array $user, array $workflow)
{
if ($user['Role']['perm_site_admin']) {
return true;
}
if (empty($workflow['Workflow'])) {
return __('Could not find associated workflow');
}
if ($workflow['Workflow']['user_id'] != $user['id']) {
return __('Only the creator user of the workflow can modify it');
}
return true;
}
/**
* attachWorkflowsToTriggers Collect the workflows listening to this trigger
*
* @param array $user
* @param array $triggers
* @param bool $group_per_blocking Whether or not the workflows should be grouped together if they have a blocking path set
* @return array
*/
public function attachWorkflowsToTriggers(array $user, array $triggers, bool $group_per_blocking=true): array
public function attachWorkflowToTriggers(array $triggers): array
{
$all_workflow_ids = [];
$workflows_per_trigger = [];
$ordered_workflows_per_trigger = [];
foreach ($triggers as $trigger) {
$workflow_ids_for_trigger = $this->__getWorkflowsIDPerTrigger($trigger['id']);
$workflows_per_trigger[$trigger['id']] = $workflow_ids_for_trigger;
$ordered_workflows_per_trigger[$trigger['id']] = $this->__getOrderedWorkflowsPerTrigger($trigger['id']);
foreach ($workflow_ids_for_trigger as $id) {
$all_workflow_ids[$id] = true;
}
}
$all_workflow_ids = array_keys($all_workflow_ids);
$workflows = $this->fetchWorkflows($user, [
// $all_workflow_ids = [];
// $workflows_per_trigger = [];
// $ordered_workflows_per_trigger = [];
// foreach ($triggers as $trigger) {
// $workflow_ids_for_trigger = $this->__getWorkflowsIDPerTrigger($trigger['id']);
// $workflows_per_trigger[$trigger['id']] = $workflow_ids_for_trigger;
// $ordered_workflows_per_trigger[$trigger['id']] = $this->__getOrderedWorkflowsPerTrigger($trigger['id']);
// foreach ($workflow_ids_for_trigger as $id) {
// $all_workflow_ids[$id] = true;
// }
// }
// $all_workflow_ids = array_keys($all_workflow_ids);
// $workflows = $this->fetchWorkflows([
// 'conditions' => [
// 'Workflow.id' => $all_workflow_ids,
// ],
// 'fields' => ['*'],
// ]);
// $workflows = Hash::combine($workflows, '{n}.Workflow.id', '{n}');
// foreach ($triggers as $i => $trigger) {
// $workflow_ids = $workflows_per_trigger[$trigger['id']];
// $ordered_workflow_ids = $ordered_workflows_per_trigger[$trigger['id']];
// $triggers[$i]['Workflows'] = [];
// foreach ($workflow_ids as $workflow_id) {
// $triggers[$i]['Workflows'][] = $workflows[$workflow_id];
// }
// if (!empty($group_per_blocking)) {
// $triggers[$i]['GroupedWorkflows'] = $this->groupWorkflowsPerBlockingType($triggers[$i]['Workflows'], $trigger['id'], $ordered_workflow_ids);
// }
// }
// return $triggers;
$workflows = $this->fetchWorkflows([
'conditions' => [
'Workflow.id' => $all_workflow_ids,
'Workflow.trigger_id' => Hash::extract($triggers, '{n}.id'),
],
'fields' => ['*'],
'contain' => ['Organisation' => ['fields' => ['*']]],
]);
$workflows = Hash::combine($workflows, '{n}.Workflow.id', '{n}');
$workflows_per_trigger = Hash::combine($workflows, '{n}.Workflow.trigger_id', '{n}');
foreach ($triggers as $i => $trigger) {
$workflow_ids = $workflows_per_trigger[$trigger['id']];
$ordered_workflow_ids = $ordered_workflows_per_trigger[$trigger['id']];
$triggers[$i]['Workflows'] = [];
foreach ($workflow_ids as $workflow_id) {
$triggers[$i]['Workflows'][] = $workflows[$workflow_id];
}
if (!empty($group_per_blocking)) {
$triggers[$i]['GroupedWorkflows'] = $this->groupWorkflowsPerBlockingType($triggers[$i]['Workflows'], $trigger['id'], $ordered_workflow_ids);
if (!empty($workflows_per_trigger[$trigger['id']])) {
$triggers[$i]['Workflow'] = $workflows_per_trigger[$trigger['id']]['Workflow'];
}
}
return $triggers;
}
public function fetchWorkflowsForTrigger($user, $trigger_id, $filterDisabled=false): array
public function fetchWorkflowsForTrigger($trigger_id, $filterDisabled=false): array
{
$workflow_ids_for_trigger = $this->__getWorkflowsIDPerTrigger($trigger_id);
$conditions = [
@ -366,10 +342,9 @@ class Workflow extends AppModel
if (!empty($filterDisabled)) {
$conditions['Workflow.enabled'] = true;
}
$workflows = $this->fetchWorkflows($user, [
$workflows = $this->fetchWorkflows([
'conditions' => $conditions,
'fields' => ['*'],
'contain' => ['Organisation' => ['fields' => ['*']], 'User' => ['Role']],
]);
return $workflows;
}
@ -377,16 +352,15 @@ class Workflow extends AppModel
/**
* getExecutionOrderForTrigger Generate the execution order for the provided trigger
*
* @param array $user
* @param array $trigger
* @return array
*/
public function getExecutionOrderForTrigger(array $user, array $trigger, $filterDisabled=false): array
public function getExecutionOrderForTrigger(array $trigger, $filterDisabled=false): array
{
if (empty($trigger)) {
return ['blocking' => [], 'non-blocking' => [] ];
}
$workflows = $this->fetchWorkflowsForTrigger($user, $trigger['id'], $filterDisabled);
$workflows = $this->fetchWorkflowsForTrigger($trigger['id'], $filterDisabled);
$ordered_workflow_ids = $this->__getOrderedWorkflowsPerTrigger($trigger['id']);
return $this->groupWorkflowsPerBlockingType($workflows, $trigger['id'], $ordered_workflow_ids);
}
@ -469,7 +443,6 @@ class Workflow extends AppModel
{
$this->loadAllWorkflowModules();
$user = ['Role' => ['perm_site_admin' => true]];
if (empty($this->loaded_modules['trigger'][$trigger_id])) {
throw new TriggerNotFoundException(__('Unknown trigger `%s`', $trigger_id));
}
@ -479,13 +452,13 @@ class Workflow extends AppModel
}
$blockingPathExecutionSuccess = true;
$workflowExecutionOrder = $this->getExecutionOrderForTrigger($user, $trigger, true);
$workflowExecutionOrder = $this->getExecutionOrderForTrigger($trigger, true);
$orderedBlockingWorkflows = $workflowExecutionOrder['blocking'];
$orderedDeferredWorkflows = $workflowExecutionOrder['non-blocking'];
foreach ($orderedBlockingWorkflows as $workflow) {
$walkResult = [];
$continueExecution = $this->walkGraph($workflow, $trigger_id, 'blocking', $data, $blockingErrors, $walkResult);
$this->loadLog()->createLogEntry($this->User->getAuthUser($workflow['User']['id']), 'walkGraph', 'Workflow', $workflow['Workflow']['id'], __('Executed blocking path for trigger `%s`', $trigger_id), $this->digestExecutionResult($walkResult));
// $this->loadLog()->createLogEntry($this->User->getAuthUser($workflow['User']['id']), 'walkGraph', 'Workflow', $workflow['Workflow']['id'], __('Executed blocking path for trigger `%s`', $trigger_id), $this->digestExecutionResult($walkResult));
if (!$continueExecution) {
$blockingPathExecutionSuccess = false;
break;
@ -495,7 +468,7 @@ class Workflow extends AppModel
$deferredErrors = [];
$walkResult = [];
$this->walkGraph($workflow, $trigger_id, 'non-blocking', $data, $deferredErrors, $walkResult);
$this->loadLog()->createLogEntry($this->User->getAuthUser($workflow['User']['id']), 'walkGraph', 'Workflow', $workflow['Workflow']['id'], __('Executed non-blocking path for trigger `%s`', $trigger_id), $this->digestExecutionResult($walkResult));
// $this->loadLog()->createLogEntry($this->User->getAuthUser($workflow['User']['id']), 'walkGraph', 'Workflow', $workflow['Workflow']['id'], __('Executed non-blocking path for trigger `%s`', $trigger_id), $this->digestExecutionResult($walkResult));
}
return $blockingPathExecutionSuccess;
}
@ -527,7 +500,7 @@ class Workflow extends AppModel
'Executed nodes' => [],
'Nodes that stopped execution' => [],
];
$workflowUser = $this->User->getAuthUser($workflow['Workflow']['user_id'], true);
// $workflowUser = $this->User->getAuthUser($workflow['Workflow']['user_id'], true);
$roamingData = $this->workflowGraphTool->getRoamingData($workflowUser, $data);
$graphData = !empty($workflow['Workflow']) ? $workflow['Workflow']['data'] : $workflow['data'];
$startNode = $this->workflowGraphTool->getNodeIdForTrigger($graphData, $trigger_id);
@ -604,7 +577,7 @@ class Workflow extends AppModel
return $moduleClass;
}
public function attachNotificationToModules(array $user, array $modules, array $workflow): array
public function attachNotificationToModules(array $modules, array $workflow): array
{
foreach ($modules as $moduleType => $modulesByType) {
foreach ($modulesByType as $i => $module) {
@ -615,24 +588,6 @@ class Workflow extends AppModel
];
}
}
$triggers = $modules['blocks_trigger'];
foreach ($triggers as $i => $trigger) {
$blockingExecutionOrder = $this->getExecutionOrderForTrigger($user, $trigger, true)['blocking'];
$blockingExecutionOrderIDs = Hash::extract($blockingExecutionOrder, '{n}.Workflow.id');
$indexInExecutionPath = array_search($workflow['Workflow']['id'], $blockingExecutionOrderIDs);
$effectiveBlockingExecutionOrder = array_slice($blockingExecutionOrder, 0, $indexInExecutionPath);
$details = [];
foreach ($effectiveBlockingExecutionOrder as $workflow) {
$details[] = sprintf('[%s] %s', h($workflow['Workflow']['id']), h($workflow['Workflow']['name']));
}
if (!empty($effectiveBlockingExecutionOrder)) {
$modules['blocks_trigger'][$i]['notifications']['warning'][] = [
'text' => __('%s blocking worflows are executed before this trigger.', count($effectiveBlockingExecutionOrder)),
'description' => __('The blocking path of this trigger might not be executed. If any of the blocking workflows stop the propagation, the blocking path of this trigger will not be executed. Nevertheless, the deferred path will always be executed.'),
'details' => $details,
];
}
}
return $modules;
}
@ -650,8 +605,7 @@ class Workflow extends AppModel
$this->loaded_modules[$type] = $classModuleFromFiles['classConfigs'];
$this->loaded_classes[$type] = $classModuleFromFiles['instancedClasses'];
}
$superUser = ['Role' => ['perm_site_admin' => true]];
$modules_from_service = $this->__getModulesFromModuleService($superUser) ?? [];
$modules_from_service = $this->__getModulesFromModuleService() ?? [];
$misp_module_class = $this->__getClassForMispModule($modules_from_service);
$misp_module_configs = [];
foreach ($misp_module_class as $i => $module_class) {
@ -687,12 +641,12 @@ class Workflow extends AppModel
/* FIXME: end */
}
private function __getEnabledModulesFromModuleService($user)
private function __getEnabledModulesFromModuleService()
{
if (empty($this->Module)) {
$this->Module = ClassRegistry::init('Module');
}
$enabledModules = $this->Module->getEnabledModules($user, null, 'Action');
$enabledModules = $this->Module->getEnabledModules(null, 'Action');
$misp_module_config = empty($enabledModules) ? false : $enabledModules;
return $misp_module_config;
}
@ -848,8 +802,8 @@ class Workflow extends AppModel
'blocks_action' => $blocks_action,
];
if (!empty($module_type)) {
if (!empty($modules[$module_type])) {
return $modules['block_' . $module_type];
if (!empty($modules['blocks_' . $module_type])) {
return $modules['blocks_' . $module_type];
} else {
return [];
}
@ -890,17 +844,15 @@ class Workflow extends AppModel
}
/**
* fetchWorkflows ACL-aware method. Basically find with ACL
* fetchWorkflows
*
* @param array $user
* @param array $options
* @param bool $full
* @return array
*/
public function fetchWorkflows(array $user, array $options = array(), $full = false)
public function fetchWorkflows(array $options = array(), $full = false)
{
$params = array(
'conditions' => $this->buildACLConditions($user),
'contain' => $this->defaultContain,
'recursive' => -1
);
@ -927,29 +879,55 @@ class Workflow extends AppModel
}
/**
* fetchWorkflow ACL-aware method. Basically find with ACL
* fetchWorkflow
*
* @param array $user
* @param int|string $id
* @param bool $throwErrors
* @throws NotFoundException
* @return array
*/
public function fetchWorkflow(array $user, $id, bool $throwErrors = true)
public function fetchWorkflow($id, bool $throwErrors = true): array
{
$options = [];
if (is_numeric($id)) {
$options = ['conditions' => ["Workflow.id" => $id]];
$options = ['conditions' => ['Workflow.id' => $id]];
} elseif (Validation::uuid($id)) {
$options = ['conditions' => ["Workflow.uuid" => $id]];
$options = ['conditions' => ['Workflow.uuid' => $id]];
} else {
if ($throwErrors) {
throw new NotFoundException(__('Invalid workflow'));
}
return [];
}
$workflow = $this->fetchWorkflows($user, $options);
$workflow = $this->fetchWorkflows($options);
if (empty($workflow)) {
throw new NotFoundException(__('Invalid workflow'));
if ($throwErrors) {
throw new NotFoundException(__('Invalid workflow'));
}
return [];
}
return $workflow[0];
}
/**
* fetchWorkflowByTrigger
*
* @param int|string $id
* @param bool $throwErrors
* @throws NotFoundException
* @return array
*/
public function fetchWorkflowByTrigger($trigger_id, bool $throwErrors = true): array
{
$options = ['conditions' => [
'Workflow.trigger_id' => $trigger_id,
]];
$workflow = $this->fetchWorkflows($options);
if (empty($workflow)) {
if ($throwErrors) {
throw new NotFoundException(__('Invalid workflow'));
}
return [];
}
return $workflow[0];
}
@ -957,18 +935,17 @@ class Workflow extends AppModel
/**
* editWorkflow Edit a worflow
*
* @param array $user
* @param array $workflow
* @return array Any errors preventing the edition
*/
public function editWorkflow(array $user, array $workflow)
public function editWorkflow(array $workflow)
{
$errors = array();
if (!isset($workflow['Workflow']['uuid'])) {
$errors[] = __('Workflow doesn\'t have an UUID');
return $errors;
}
$existingWorkflow = $this->fetchWorkflow($user, $workflow['Workflow']['id']);
$existingWorkflow = $this->fetchWorkflow($workflow['Workflow']['id']);
$workflow['Workflow']['id'] = $existingWorkflow['Workflow']['id'];
unset($workflow['Workflow']['timestamp']);
$errors = $this->__saveAndReturnErrors($workflow, ['fieldList' => self::CAPTURE_FIELDS], $errors);
@ -978,16 +955,15 @@ class Workflow extends AppModel
/**
* fetchWorkflow ACL-aware method. Basically find with ACL
*
* @param array $user
* @param int|string $id
* @param bool $enable
* @param bool $throwErrors
* @return array
*/
public function toggleWorkflow(array $user, $id, $enable=true, bool $throwErrors=true)
public function toggleWorkflow($id, $enable=true, bool $throwErrors=true)
{
$errors = array();
$workflow = $this->fetchWorkflow($user, $id, $throwErrors);
$workflow = $this->fetchWorkflow($id, $throwErrors);
$workflow['Workflow']['enabled'] = $enable;
$errors = $this->__saveAndReturnErrors($workflow, ['fieldList' => ['enabled']], $errors);
return $errors;

View File

@ -8,7 +8,7 @@ class Module_email_sent extends WorkflowBaseModule
public $description = 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.';
public $icon = 'envelope';
public $inputs = 0;
public $outputs = 2;
public $outputs = 1;
public function __construct()
{

View File

@ -8,7 +8,7 @@ class Module_feed_pull extends WorkflowBaseModule
public $description = 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.';
public $icon = 'arrow-alt-circle-down';
public $inputs = 0;
public $outputs = 2;
public $outputs = 1;
public function __construct()
{

View File

@ -8,7 +8,7 @@ class Module_new_attribute extends WorkflowBaseModule
public $description = 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.';
public $icon = 'cube';
public $inputs = 0;
public $outputs = 2;
public $outputs = 1;
public function __construct()
{

View File

@ -8,7 +8,7 @@ class Module_new_object extends WorkflowBaseModule
public $description = 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.';
public $icon = 'cubes';
public $inputs = 0;
public $outputs = 2;
public $outputs = 1;
public function __construct()
{

View File

@ -8,7 +8,7 @@ class Module_new_user extends WorkflowBaseModule
public $description = 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.';
public $icon = 'user-plus';
public $inputs = 0;
public $outputs = 2;
public $outputs = 1;
public function __construct()
{

View File

@ -8,7 +8,7 @@ class Module_publish extends WorkflowBaseModule
public $description = 'Lorem ipsum dolor, sit amet consectetur adipisicing elit.';
public $icon = 'upload';
public $inputs = 0;
public $outputs = 2;
public $outputs = 1;
public function __construct()
{

View File

@ -85,7 +85,7 @@ echo $this->element('genericElements/assetLoader', [
});
var centroidX = sumX / nodes.length
var centroidY = sumY / nodes.length
var calc_zoom = Math.min(editor_bcr.width / maxX, editor_bcr.height / maxY) // Zoom out if needed
var calc_zoom = Math.min(Math.min(editor_bcr.width / maxX, editor_bcr.height / maxY), 1) // Zoom out if needed
editor.translate_to(
offset_x - centroidX + offset_block_x,
offset_y - centroidY - offset_block_y

View File

@ -1615,8 +1615,8 @@ $divider = $this->element('/genericElements/SideMenu/side_menu_divider');
case 'workflows':
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'index',
'url' => '/workflows/index',
'text' => __('List Workflows')
'url' => '/workflows/triggers',
'text' => __('List Triggers')
));
echo $this->element('/genericElements/SideMenu/side_menu_link', array(
'element_id' => 'index_trigger',

View File

@ -1,8 +1,10 @@
<?php
$allModules = [];
foreach ($modules as $moduleType => $module) {
$allModules = array_merge($allModules, $module);
}
$usableModules = [
'blocks_action' => $modules['blocks_action'],
'blocks_logic' => $modules['blocks_logic'],
];
$allModules = array_merge($usableModules['blocks_action'], $usableModules['blocks_logic']);
$triggerModules = $modules['blocks_trigger'];
?>
<div class="root-container">
<div class="main-container">
@ -11,18 +13,13 @@ foreach ($modules as $moduleType => $module) {
<i class="fa-fw <?= $this->FontAwesome->getClass('caret-left') ?>"></i>
<?= __('Workflow index') ?>
</a>
<h3>Workflows</h3>
<div class="workflow-selector-container">
<select type="text" placeholder="Load a workflow" class="chosen-container workflows" autocomplete="off">
<?php foreach ($workflows as $workflow) : ?>
<option value="<?= h($workflow['Workflow']['id']) ?>" <?= $selectedWorkflow['Workflow']['id'] == $workflow['Workflow']['id'] ? 'selected' : '' ?>><?= h($workflow['Workflow']['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<h3>
<span style="font-weight:normal;"><?= __('Workflows:') ?></span>
<strong><?= h($selectedWorkflow['Workflow']['trigger_id']) ?></strong>
</h3>
<div class="" style="margin-top: 0.5em;">
<div class="btn-group" style="margin-left: 3px;">
<a class="btn btn-primary" href="<?= $baseurl . '/workflows/add' ?>"><i class="fa-fw <?= $this->FontAwesome->getClass('plus') ?>"></i> <?= __('New') ?></a>
<a class="btn btn-primary dropdown-toggle" data-toggle="dropdown" href="#"><span class="caret"></span></a>
<a class="btn btn-primary dropdown-toggle" data-toggle="dropdown" href="#"><?= __('More Actions') ?> <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a id="importWorkflow" href="<?= $baseurl . '/workflows/import/' ?>"><i class="fa-fw <?= $this->FontAwesome->getClass('file-import') ?>"></i> <?= __('Import workflow') ?></a></li>
<li><a id="exportWorkflow" href="<?= $baseurl . '/workflows/export/' . h($selectedWorkflow['Workflow']['id']) ?>"><i class="fa-fw <?= $this->FontAwesome->getClass('file-export') ?>"></i> <?= __('Export workflow') ?></a></li>
@ -45,23 +42,19 @@ foreach ($modules as $moduleType => $module) {
</select>
<ul class="nav nav-tabs" id="block-tabs">
<li class="active"><a href="#container-triggers">
<i class="<?= $this->FontAwesome->getClass('flag') ?>"></i>
Triggers
<li class="active"><a href="#container-actions">
<i class="<?= $this->FontAwesome->getClass('play') ?>"></i>
Actions
</a></li>
<li><a href="#container-logic">
<i class="<?= $this->FontAwesome->getClass('code-branch') ?>"></i>
Logic
</a></li>
<li><a href="#container-actions">
<i class="<?= $this->FontAwesome->getClass('play') ?>"></i>
Actions
</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="container-triggers">
<?php foreach ($modules['blocks_trigger'] as $block) : ?>
<div class="tab-pane active" id="container-actions">
<?php foreach ($modules['blocks_action'] as $block) : ?>
<?= $this->element('Workflows/sidebar-block', ['block' => $block]) ?>
<?php endforeach; ?>
</div>
@ -70,11 +63,6 @@ foreach ($modules as $moduleType => $module) {
<?= $this->element('Workflows/sidebar-block', ['block' => $block]) ?>
<?php endforeach; ?>
</div>
<div class="tab-pane" id="container-actions">
<?php foreach ($modules['blocks_action'] as $block) : ?>
<?= $this->element('Workflows/sidebar-block', ['block' => $block]) ?>
<?php endforeach; ?>
</div>
</div>
</div>
@ -138,14 +126,14 @@ echo $this->element('genericElements/assetLoader', [
var $exportWorkflowButton = $('#exportWorkflow')
var $saveWorkflowButton = $('#saveWorkflow')
var $lastModifiedField = $('#lastModifiedField')
var $blockContainerTriggers = $('#container-triggers')
var $blockContainerLogic = $('#container-logic')
var $blockContainerAction = $('#container-actions')
var editor = false
var all_blocks = <?= json_encode($allModules) ?>;
var all_blocks_by_id = <?= json_encode(Hash::combine($allModules, '{n}.id', '{n}')) ?>;
var all_triggers_by_id = <?= json_encode(Hash::combine($triggerModules, '{n}.id', '{n}')) ?>;
var workflow = false
<?php if (!empty($workflow)) : ?>
<?php if (!empty($selectedWorkflow)) : ?>
var workflow = <?= json_encode($selectedWorkflow) ?>;
<?php endif; ?>

View File

@ -15,12 +15,10 @@
'data_path' => 'description',
],
[
'name' => __('Workflow Execution Order'),
'requirement' => $indexType == 'trigger',
'element' => 'custom',
'function' => function ($row) {
return $this->element('Workflows/executionOrder', ['trigger' => $row]);
}
'name' => __('Module Type'),
'sort' => 'module_type',
'class' => 'short',
'data_path' => 'module_type',
],
[
'name' => __('Is misp-module'),
@ -54,26 +52,21 @@
[
'type' => 'simple',
'children' => [
[
'url' => $baseurl . '/workflows/moduleIndex/type:trigger',
'text' => __('Triggers'),
'active' => $indexType === 'trigger',
],
[
'url' => $baseurl . '/workflows/moduleIndex/type:all',
'text' => __('All'),
'active' => $indexType === 'all',
],
[
'url' => $baseurl . '/workflows/moduleIndex/type:logic',
'text' => __('Logic'),
'active' => $indexType === 'logic',
],
[
'url' => $baseurl . '/workflows/moduleIndex/type:action',
'text' => __('Action'),
'active' => $indexType === 'action',
],
[
'url' => $baseurl . '/workflows/moduleIndex/type:logic',
'text' => __('Logic'),
'active' => $indexType === 'logic',
],
]
],
[

View File

@ -1,18 +1,8 @@
<?php
$append = [];
if ($data['module_type'] == 'trigger') {
$append[] = [
'element' => 'Workflows/executionOrderWidget',
'element_params' => [
'trigger' => $data
]
];
}
echo $this->element(
'genericElements/SingleViews/single_view',
[
'title' => 'Workflow module view',
'title' => $data['module_type'] == 'trigger' ? __('Trigger module view') : __('Workflow module view'),
'data' => $data,
'fields' => [
[
@ -57,16 +47,32 @@ echo $this->element(
'path' => 'params',
],
[
'key' => __('Workflow Execution Order'),
'requirement' => $data['module_type'] == 'trigger',
'key' => __('Run counter'),
'path' => 'Workflow.counter',
'type' => 'custom',
'function' => function ($row) {
return $this->element('Workflows/executionOrder', ['trigger' => $row]);
return h($row['Workflow']['counter']);
}
],
[
'key' => __('Workflow Data'),
'class' => 'restrict-height',
'path' => 'Workflow.data',
'type' => 'json',
],
],
'append' => $append
'append' => [
['element' => 'Workflows/executionPath', 'element_params' => ['workflow' => $data['Workflow']]],
]
]
);
?>
<style>
.restrict-height>div {
height: 200px;
overflow: auto;
resize: both;
}
</style>

View File

@ -0,0 +1,89 @@
<?php
$fields = [
[
'name' => __('Module name'),
'sort' => 'name',
'data_path' => 'name',
'element' => 'custom',
'class' => 'bold',
'function' => function ($row) {
return sprintf('<i class="fa-fw %s"></i> %s', $this->FontAwesome->getClass($row['icon']), h($row['name']));
}
],
[
'name' => __('Description'),
'data_path' => 'description',
],
[
'name' => __('Module Enabled'),
'sort' => 'disabled',
'class' => 'short',
'data_path' => 'disabled',
'element' => 'booleanOrNA',
'boolean_reverse' => true
],
];
echo $this->element('genericElements/IndexTable/scaffold', [
'scaffold_data' => [
'data' => [
'stupid_pagination' => true,
'data' => $data,
'top_bar' => [
],
'fields' => $fields,
'icon' => 'flag',
'title' => __('Triggers'),
'description' => __('List the available triggers that can be listened to by workflows'),
'actions' => [
[
'title' => __('Enable'),
'icon' => 'play',
'postLink' => true,
'url' => $baseurl . '/workflows/enableModule',
'url_params_data_paths' => ['id'],
'postLinkConfirm' => __('Are you sure you want to enable this module?'),
'complex_requirement' => array(
'function' => function ($row, $options) use ($isSiteAdmin) {
return $isSiteAdmin && $options['datapath']['disabled'];
},
'options' => array(
'datapath' => array(
'disabled' => 'disabled'
)
)
),
],
[
'title' => __('Disable'),
'icon' => 'stop',
'postLink' => true,
'url' => $baseurl . '/workflows/disableModule',
'url_params_data_paths' => ['id'],
'postLinkConfirm' => __('Are you sure you want to disable this module?'),
'complex_requirement' => array(
'function' => function ($row, $options) use ($isSiteAdmin) {
return $isSiteAdmin && !$options['datapath']['disabled'];
},
'options' => array(
'datapath' => array(
'disabled' => 'disabled'
)
)
),
],
[
'url' => $baseurl . '/workflows/editor',
'url_params_data_paths' => ['id'],
'icon' => 'code',
'dbclickAction' => true,
],
[
'url' => $baseurl . '/workflows/moduleView',
'url_params_data_paths' => ['id'],
'icon' => 'eye',
],
]
]
]
]);

View File

@ -21,13 +21,6 @@ echo $this->element(
'key' => __('Timestamp'),
'path' => 'Workflow.timestamp',
],
[
'key' => __('Owner Organisation'),
'path' => 'Workflow.org_id',
'pathName' => 'Organisation.name',
'type' => 'model',
'model' => 'organisations'
],
[
'key' => __('Description'),
'path' => 'Workflow.description'

View File

@ -224,6 +224,12 @@
/* No special format for default block types */
}
.drawflow .drawflow-node.block-type-trigger {
border-left-color: #73a2c9;
border-left-width: 0.25rem;
border-left-style: solid;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output::before {
display: none;
transform: translateX(calc(50% - 8px));
@ -256,29 +262,19 @@
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_1 {
background-color: #ea5b57;
background-color: #73a2c9;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_1::before {
content: 'blocking';
content: 'Listen';
top: -26px;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_1::after {
content: "\f251";
content: "\f025";
font-size: 9px;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_1:hover::after {
font-size: 14px;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_2 {
background-color: #468847;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_2::before {
content: 'deferred';
top: 26px;
}
.drawflow .drawflow-node.block-type-trigger > .outputs > .output_2::after {
content: "\f074";
}
.drawflow .drawflow-node.block-type-IF > .outputs > .output::before {
display: none;

View File

@ -20,7 +20,22 @@ var dotBlock_default = doT.template(' \
</div> \
</div>')
var dotBlock_trigger = dotBlock_default
var dotBlock_trigger = doT.template(' \
<div class="canvas-workflow-block" data-nodeuid="{{=it.node_uid}}"> \
<div style="width: 100%;"> \
<div class="default-main-container" style="border:none;"> \
<i class="fa-fw fa-{{=it.icon}} {{=it.icon_class}}"></i> \
<strong style="margin-left: 0.25em;"> \
{{=it.name}} \
</strong> \
<span style="margin-left: auto;"> \
<span class="block-notification-container"> \
{{=it._block_notification_html}} \
</span> \
</span> \
</div> \
</div> \
</div>')
var dotBlock_IF = doT.template(' \
<div class="canvas-workflow-block" data-nodeuid="{{=it.node_uid}}"> \
@ -99,11 +114,9 @@ function initDrawflow() {
editor.on('nodeCreated', function() {
invalidateContentCache()
toggleTriggersDraggableState()
})
editor.on('nodeRemoved', function () {
invalidateContentCache()
toggleTriggersDraggableState()
})
editor.on('nodeDataChanged', invalidateContentCache)
editor.on('nodeMoved', invalidateContentCache)
@ -120,7 +133,7 @@ function initDrawflow() {
}
})
editor.on('keydown', function (evt) {
if (evt.keyCode == 67) {
if (evt.keyCode == 67 && $drawflow.is(evt.target)) {
editor.fitCanvas()
}
})
@ -227,6 +240,14 @@ function initDrawflow() {
fetchAndLoadWorkflow().then(function() {
graphPooler.start(undefined)
editor.fitCanvas()
// block contextual menu for trigger blocks
$canvas.find('.canvas-workflow-block').on('contextmenu', function (evt) {
var selectedNode = getSelectedBlock()
if (selectedNode !== undefined && selectedNode.data.module_type == 'trigger') {
evt.stopPropagation();
evt.preventDefault();
}
})
})
$saveWorkflowButton.click(saveWorkflow)
$importWorkflowButton.click(importWorkflow)
@ -301,16 +322,11 @@ function revalidateContentCache() {
function addNode(block, position) {
var module = all_blocks_by_id[block.id]
var module = all_blocks_by_id[block.id] || all_triggers_by_id[block.id]
if (!module) {
console.error('Tried to add node for unknown module ' + block.data.id + ' (' + block.id + ')')
return '';
}
if (editor.registeredTriggers[module.id]) {
console.info('Tried to add node for a trigger already registered: ' + module.id)
return '';
}
var node_uid = uid() // only used for UI purposes
block['node_uid'] = node_uid
@ -340,50 +356,6 @@ function addNode(block, position) {
);
}
function toggleTriggersDraggableState() {
if (editor.isLoading) {
return
}
var data = Object.values(getEditorData())
editor.registeredTriggers = {}
data.forEach(function(node) {
if (node.data.module_type == 'trigger') {
editor.registeredTriggers[node.data.id] = true
}
})
$blockContainerTriggers.find('.sidebar-workflow-block')
.filter(function () {
return !$(this).hasClass('ui-draggable-dragging')
&& !$(this).data('block').disabled
})
.draggable('option', { disabled: false })
.removeClass(['disabled', 'disabled-one-instance'])
.attr('title', '')
data.forEach(function(node) {
if (node.data.module_type == 'trigger') {
$blockContainerTriggers.find('#'+node.data.id + '.sidebar-workflow-block')
.filter(function () {
return !$(this).hasClass('ui-draggable-dragging')
|| $(this).data('block').disabled
|| editor.registeredTriggers[$(this).data('block').id] !== undefined
})
.draggable('option', { disabled: true })
.addClass(['disabled', 'disabled-one-instance'])
.attr('title', 'Only one instance of this trigger is allowed per workflow')
.each(function() {
var block_id = $(this).data('block').id
$chosenBlocks.find('option')
.filter(function() {
return $(this).val() == block_id
})
.prop('disabled', true)
$chosenBlocks.chosen('destroy').chosen()
})
}
})
}
function getEditorData(cleanInvalidParams) {
var data = {} // Make sure nodes are index by their internal IDs
var editorExport = editor.export().drawflow.Home.data
@ -416,7 +388,6 @@ function fetchAndLoadWorkflow() {
lastModified = workflow.timestamp + '000'
loadWorkflow(workflow)
editor.isLoading = false
toggleTriggersDraggableState()
revalidateContentCache()
resolve()
})
@ -425,10 +396,21 @@ function fetchAndLoadWorkflow() {
function loadWorkflow(workflow) {
editor.clear()
if (workflow.data.length == 0) {
console.log('stop');
var trigger_id = workflow['trigger_id'];
if (all_triggers_by_id[trigger_id] === undefined) {
console.error('Unknown trigger');
showMessage('error', 'Unknown trigger')
}
var trigger_block = all_triggers_by_id[trigger_id]
addNode(trigger_block, {left: 0, top: 0})
}
// We cannot rely on the editor's import function as it recreates the nodes with the saved HTML instead of rebuilding them
// We have to manually add the nodes and their connections
Object.values(workflow.data).forEach(function (block) {
if (!all_blocks_by_id[block.data.id]) {
var module = all_blocks_by_id[block.data.id] || all_triggers_by_id[block.data.id]
if (!module) {
console.error('Tried to add node for unknown module ' + block.data.id + ' (' + block.id + ')')
return '';
}
@ -924,7 +906,7 @@ function setParamValueForInput($input, node_data) {
}
function genBlockNotificationHtml(block) {
var module = all_blocks_by_id[block.id]
var module = all_blocks_by_id[block.id] || all_triggers_by_id[block.id]
if (!module) {
console.error('Tried to get notification of unknown module ' + block.id)
return '';