diff --git a/Makefile b/Makefile index dc3475a81..75dfaf097 100644 --- a/Makefile +++ b/Makefile @@ -151,9 +151,6 @@ mocks: $(GOMOCK) ######################################################################################################################## # Toolset -$(GOTEST): - $(GOGET) github.com/rakyll/gotest - $(REALIZE): $(GOGET) github.com/tockins/realize diff --git a/api/system/spec.json b/api/system/spec.json index e5522c3b3..cd45c1b97 100644 --- a/api/system/spec.json +++ b/api/system/spec.json @@ -1103,7 +1103,8 @@ } } ] - }, { + }, + { "title": "Permissions", "parameters": {}, "entrypoint": "permissions", @@ -1200,5 +1201,385 @@ } } ] + }, + { + "title": "Automation scripts", + "entrypoint": "automation_script", + "path": "/automation/script", + "authentication": [ + "Client ID", + "Session ID" + ], + + "struct": [ + { + "imports": [ + "github.com/cortezaproject/corteza-server/pkg/automation" + ] + } + ], + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List/read automation script", + "path": "/", + "parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query to match against automation script", + "type": "string" + }, + { + "name": "resource", + "required": false, + "title": "Limit by resource (via trigger)", + "type": "string" + }, + { + "name": "incDeleted", + "required": false, + "title": "Include deleted scripts", + "type": "bool" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" + } + ] + } + }, + { + "name": "create", + "method": "POST", + "title": "Add new automation script", + "path": "/", + "parameters": { + "post": [ + { + "name": "name", + "title": "automation name", + "type": "string" + }, + { + "name": "sourceRef", + "title": "Source URL", + "type": "string" + }, + { + "name": "source", + "title": "Source code", + "type": "string" + }, + { + "name": "runAs", + "title": "Run as specific user", + "type": "uint64" + }, + { + "name": "timeout", + "title": "Script timeout (in milliseconds)", + "type": "uint" + }, + { + "name": "critical", + "title": "Is it critical to run this script successfully", + "type": "bool" + }, + { + "name": "async", + "title": "Will this script be ran asynchronously", + "type": "bool" + }, + { + "name": "enabled", + "type": "bool" + }, + { + "name": "triggers", + "type": "automation.TriggerSet" + } + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read automation script by ID", + "path": "/{scriptID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "automation script ID" + } + ] + } + }, + { + "name": "update", + "method": "POST", + "title": "Update automation script", + "path": "/{scriptID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "Automation script ID" + } + ], + "post": [ + { + "name": "name", + "title": "Script name", + "type": "string" + }, + { + "name": "sourceRef", + "title": "Source URL", + "type": "string" + }, + { + "name": "source", + "title": "Source code", + "type": "string" + }, + { + "name": "runAs", + "title": "Run script as specific user", + "type": "uint64" + }, + { + "name": "timeout", + "title": "Run script in user-agent (browser)", + "type": "uint" + }, + { + "name": "critical", + "title": "Is it critical to run this script successfully", + "type": "bool" + }, + { + "name": "async", + "title": "Will this script be ran asynchronously", + "type": "bool" + }, + { + "name": "enabled", + "type": "bool" + }, + { + "name": "triggers", + "type": "automation.TriggerSet" + } + ] + } + }, + { + "name": "delete", + "method": "DELETE", + "title": "Delete script", + "path": "/{scriptID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "Script ID" + } + ] + } + }, + { + "name": "test", + "method": "POST", + "title": "Run source code in corredor. Used for testing", + "path": "/test", + "parameters": { + "post": [ + {"name": "source", "type": "string", "title": "Script's source code"}, + {"name": "payload", "type": "json.RawMessage", "title": "Payload to be used"} + ] + } + } + ] + }, + { + "title": "Automation script triggers", + "entrypoint": "automation_trigger", + "path": "/automation/script/{scriptID}/trigger", + "authentication": [ + "Client ID", + "Session ID" + ], + "parameters": { + "path": [ + { + "type": "uint64", + "name": "scriptID", + "required": true, + "title": "Script ID" + } + ] + }, + "apis": [ + { + "name": "list", + "method": "GET", + "title": "List/read automation script triggers", + "path": "/", + "parameters": { + "get": [ + { + "name": "resource", + "required": false, + "title": "Only triggers of a specific resource", + "type": "string" + }, + { + "name": "event", + "required": false, + "title": "Only triggers of a specific event", + "type": "string" + }, + { + "name": "incDeleted", + "required": false, + "title": "Include deleted scripts", + "type": "bool" + }, + { + "name": "page", + "type": "uint", + "required": false, + "title": "Page number (0 based)" + }, + { + "name": "perPage", + "type": "uint", + "required": false, + "title": "Returned items per page (default 50)" + } + ] + } + }, + { + "name": "create", + "method": "POST", + "title": "Add new automation script trigger", + "path": "/", + "parameters": { + "post": [ + { + "name": "resource", + "title": "Resource", + "type": "string", + "required": true + }, + { + "name": "event", + "title": "Event", + "type": "string", + "required": true + }, + { + "name": "condition", + "title": "Event", + "type": "string" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "name": "read", + "method": "GET", + "title": "Read automation script trigger by ID", + "path": "/{triggerID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "Automation script trigger ID" + } + ] + } + }, + { + "name": "update", + "method": "POST", + "title": "Update automation script trigger", + "path": "/{triggerID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "Automation script trigger ID" + } + ], + "post": [ + { + "name": "resource", + "title": "Resource", + "type": "string", + "required": true + }, + { + "name": "event", + "title": "Event", + "type": "string", + "required": true + }, + { + "name": "condition", + "title": "Event", + "type": "string" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "name": "delete", + "method": "DELETE", + "title": "Delete script", + "path": "/{triggerID}", + "parameters": { + "path": [ + { + "type": "uint64", + "name": "triggerID", + "required": true, + "title": "Automation script trigger ID" + } + ] + } + } + ] } ] diff --git a/api/system/spec/automation_script.json b/api/system/spec/automation_script.json new file mode 100644 index 000000000..0257e5685 --- /dev/null +++ b/api/system/spec/automation_script.json @@ -0,0 +1,226 @@ +{ + "Title": "Automation scripts", + "Interface": "Automation_script", + "Struct": [ + { + "imports": [ + "github.com/cortezaproject/corteza-server/pkg/automation" + ] + } + ], + "Parameters": null, + "Protocol": "", + "Authentication": [ + "Client ID", + "Session ID" + ], + "Path": "/automation/script", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "List/read automation script", + "Path": "/", + "Parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query to match against automation script", + "type": "string" + }, + { + "name": "resource", + "required": false, + "title": "Limit by resource (via trigger)", + "type": "string" + }, + { + "name": "incDeleted", + "required": false, + "title": "Include deleted scripts", + "type": "bool" + }, + { + "name": "page", + "required": false, + "title": "Page number (0 based)", + "type": "uint" + }, + { + "name": "perPage", + "required": false, + "title": "Returned items per page (default 50)", + "type": "uint" + } + ] + } + }, + { + "Name": "create", + "Method": "POST", + "Title": "Add new automation script", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "name", + "title": "automation name", + "type": "string" + }, + { + "name": "sourceRef", + "title": "Source URL", + "type": "string" + }, + { + "name": "source", + "title": "Source code", + "type": "string" + }, + { + "name": "runAs", + "title": "Run as specific user", + "type": "uint64" + }, + { + "name": "timeout", + "title": "Script timeout (in milliseconds)", + "type": "uint" + }, + { + "name": "critical", + "title": "Is it critical to run this script successfully", + "type": "bool" + }, + { + "name": "async", + "title": "Will this script be ran asynchronously", + "type": "bool" + }, + { + "name": "enabled", + "type": "bool" + }, + { + "name": "triggers", + "type": "automation.TriggerSet" + } + ] + } + }, + { + "Name": "read", + "Method": "GET", + "Title": "Read automation script by ID", + "Path": "/{scriptID}", + "Parameters": { + "path": [ + { + "name": "scriptID", + "required": true, + "title": "automation script ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "update", + "Method": "POST", + "Title": "Update automation script", + "Path": "/{scriptID}", + "Parameters": { + "path": [ + { + "name": "scriptID", + "required": true, + "title": "Automation script ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "name", + "title": "Script name", + "type": "string" + }, + { + "name": "sourceRef", + "title": "Source URL", + "type": "string" + }, + { + "name": "source", + "title": "Source code", + "type": "string" + }, + { + "name": "runAs", + "title": "Run script as specific user", + "type": "uint64" + }, + { + "name": "timeout", + "title": "Run script in user-agent (browser)", + "type": "uint" + }, + { + "name": "critical", + "title": "Is it critical to run this script successfully", + "type": "bool" + }, + { + "name": "async", + "title": "Will this script be ran asynchronously", + "type": "bool" + }, + { + "name": "enabled", + "type": "bool" + }, + { + "name": "triggers", + "type": "automation.TriggerSet" + } + ] + } + }, + { + "Name": "delete", + "Method": "DELETE", + "Title": "Delete script", + "Path": "/{scriptID}", + "Parameters": { + "path": [ + { + "name": "scriptID", + "required": true, + "title": "Script ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "test", + "Method": "POST", + "Title": "Run source code in corredor. Used for testing", + "Path": "/test", + "Parameters": { + "post": [ + { + "name": "source", + "title": "Script's source code", + "type": "string" + }, + { + "name": "payload", + "title": "Payload to be used", + "type": "json.RawMessage" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/api/system/spec/automation_trigger.json b/api/system/spec/automation_trigger.json new file mode 100644 index 000000000..3a232f431 --- /dev/null +++ b/api/system/spec/automation_trigger.json @@ -0,0 +1,165 @@ +{ + "Title": "Automation script triggers", + "Interface": "Automation_trigger", + "Struct": null, + "Parameters": { + "path": [ + { + "name": "scriptID", + "required": true, + "title": "Script ID", + "type": "uint64" + } + ] + }, + "Protocol": "", + "Authentication": [ + "Client ID", + "Session ID" + ], + "Path": "/automation/script/{scriptID}/trigger", + "APIs": [ + { + "Name": "list", + "Method": "GET", + "Title": "List/read automation script triggers", + "Path": "/", + "Parameters": { + "get": [ + { + "name": "resource", + "required": false, + "title": "Only triggers of a specific resource", + "type": "string" + }, + { + "name": "event", + "required": false, + "title": "Only triggers of a specific event", + "type": "string" + }, + { + "name": "incDeleted", + "required": false, + "title": "Include deleted scripts", + "type": "bool" + }, + { + "name": "page", + "required": false, + "title": "Page number (0 based)", + "type": "uint" + }, + { + "name": "perPage", + "required": false, + "title": "Returned items per page (default 50)", + "type": "uint" + } + ] + } + }, + { + "Name": "create", + "Method": "POST", + "Title": "Add new automation script trigger", + "Path": "/", + "Parameters": { + "post": [ + { + "name": "resource", + "required": true, + "title": "Resource", + "type": "string" + }, + { + "name": "event", + "required": true, + "title": "Event", + "type": "string" + }, + { + "name": "condition", + "title": "Event", + "type": "string" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "Name": "read", + "Method": "GET", + "Title": "Read automation script trigger by ID", + "Path": "/{triggerID}", + "Parameters": { + "path": [ + { + "name": "triggerID", + "required": true, + "title": "Automation script trigger ID", + "type": "uint64" + } + ] + } + }, + { + "Name": "update", + "Method": "POST", + "Title": "Update automation script trigger", + "Path": "/{triggerID}", + "Parameters": { + "path": [ + { + "name": "triggerID", + "required": true, + "title": "Automation script trigger ID", + "type": "uint64" + } + ], + "post": [ + { + "name": "resource", + "required": true, + "title": "Resource", + "type": "string" + }, + { + "name": "event", + "required": true, + "title": "Event", + "type": "string" + }, + { + "name": "condition", + "title": "Event", + "type": "string" + }, + { + "name": "enabled", + "type": "bool" + } + ] + } + }, + { + "Name": "delete", + "Method": "DELETE", + "Title": "Delete script", + "Path": "/{triggerID}", + "Parameters": { + "path": [ + { + "name": "triggerID", + "required": true, + "title": "Automation script trigger ID", + "type": "uint64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/codegen.sh b/codegen.sh index 60280c296..abcb041e6 100755 --- a/codegen.sh +++ b/codegen.sh @@ -164,8 +164,8 @@ function proto { CORTEZA_PROTOBUF_PATH=${CORTEZA_PROTOBUF_PATH:-"vendor/github.com/cortezaproject/corteza-protobuf"} - # Compose Proto Path - CPC="github.com/cortezaproject/corteza-server/compose/proto" + ComposeProtoPath="github.com/cortezaproject/corteza-server/compose/proto" + SystemProtoPath="github.com/cortezaproject/corteza-server/system/proto" yellow " ${CORTEZA_PROTOBUF_PATH} >> compose/proto" PATH=$PATH:$GOPATH/bin protoc \ @@ -174,9 +174,10 @@ function proto { record.proto namespace.proto module.proto - assoc="${assoc},Mcompose/record.proto=${CPC}" - assoc="${assoc},Mcompose/module.proto=${CPC}" - assoc="${assoc},Mcompose/namespace.proto=${CPC}" + assoc="${assoc},Mcompose/record.proto=${ComposeProtoPath}" + assoc="${assoc},Mcompose/module.proto=${ComposeProtoPath}" + assoc="${assoc},Mcompose/namespace.proto=${ComposeProtoPath}" + assoc="${assoc},Msystem/mail_message.proto=${SystemProtoPath}" yellow " ${CORTEZA_PROTOBUF_PATH} >> pkg/automation/corredor" PATH=$PATH:$GOPATH/bin protoc \ @@ -188,7 +189,7 @@ function proto { PATH=$PATH:$GOPATH/bin protoc \ --proto_path ${CORTEZA_PROTOBUF_PATH}/system \ --go_out=plugins=grpc:system/proto \ - user.proto + user.proto mail_message.proto green "OK" } diff --git a/compose/db/mysql/static.go b/compose/db/mysql/static.go index 1f01dbc4b..4bc8eebb2 100644 --- a/compose/db/mysql/static.go +++ b/compose/db/mysql/static.go @@ -3,4 +3,4 @@ // Package contains static assets. package mysql -var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content` (\n `id` bigint(20) unsigned NOT NULL,\n `module_id` bigint(20) unsigned NOT NULL,\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`,`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_content_column` (\n `content_id` bigint(20) NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `column_value` text NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_field` (\n `field_type` varchar(16) NOT NULL COMMENT 'Short field type (string, boolean,...)',\n `field_name` varchar(255) NOT NULL COMMENT 'Description of field contents',\n `field_template` varchar(255) NOT NULL COMMENT 'HTML template file for field',\n PRIMARY KEY (`field_type`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module` (\n `id` bigint(20) unsigned NOT NULL,\n `name` varchar(64) NOT NULL COMMENT 'The name of the module',\n `json` json NOT NULL COMMENT 'List of field definitions for the module',\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module_form` (\n `module_id` bigint(20) unsigned NOT NULL,\n `place` tinyint(3) unsigned NOT NULL,\n `kind` varchar(64) NOT NULL COMMENT 'The type of the form input field',\n `name` varchar(64) NOT NULL COMMENT 'The name of the field in the form',\n `label` varchar(255) NOT NULL COMMENT 'The label of the form input',\n `help_text` text NOT NULL COMMENT 'Help text',\n `default_value` text NOT NULL COMMENT 'Default value',\n `max_length` int(10) unsigned NOT NULL COMMENT 'Maximum input length',\n `is_private` tinyint(1) NOT NULL COMMENT 'Contains personal/sensitive data?',\n PRIMARY KEY (`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_page` (\n `id` bigint(20) unsigned NOT NULL COMMENT 'Page ID',\n `self_id` bigint(20) unsigned NOT NULL COMMENT 'Parent Page ID',\n `module_id` bigint(20) unsigned NOT NULL COMMENT 'Module ID (optional)',\n `title` varchar(255) NOT NULL COMMENT 'Title (required)',\n `description` text NOT NULL COMMENT 'Description',\n `blocks` json NOT NULL COMMENT 'JSON array of blocks for the page',\n `visible` tinyint(4) NOT NULL COMMENT 'Is page visible in navigation?',\n `weight` int(11) NOT NULL COMMENT 'Order for navigation',\n PRIMARY KEY (`id`) USING BTREE,\n KEY `module_id` (`module_id`),\n KEY `self_id` (`self_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` VALUES ('bool','Boolean value (yes / no)','');\nINSERT INTO `crm_field` VALUES ('email','E-mail input','');\nINSERT INTO `crm_field` VALUES ('enum','Single option picker','');\nINSERT INTO `crm_field` VALUES ('hidden','Hidden value','');\nINSERT INTO `crm_field` VALUES ('stamp','Date/time input','');\nINSERT INTO `crm_field` VALUES ('text','Text input','');\nINSERT INTO `crm_field` VALUES ('textarea','Text input (multi-line)','');\nPK\x07\x08f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00 \x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `user_id` BIGINT UNSIGNED NOT NULL AFTER `module_id`, ADD INDEX (`user_id`);\nPK\x07\x08\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` (`field_type`, `field_name`, `field_template`) VALUES ('related', 'Related content', ''), ('related_multi', 'Related content (multiple)', '');PK\x07\x08:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content_links` (\n `content_id` bigint(20) unsigned NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `rel_content_id` bigint(20) unsigned NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`,`rel_content_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;PK\x07\x08\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00D\x00 \x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `is_required` TINYINT(1) NOT NULL AFTER `is_private`, ADD `is_visible` TINYINT(1) NOT NULL AFTER `is_required`;PK\x07\x08\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x005\x00 \x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` DROP PRIMARY KEY, ADD PRIMARY KEY(`module_id`, `place`);\nPK\x07\x08\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `json` json DEFAULT NULL COMMENT 'Content in JSON format.' AFTER `user_id`;\nPK\x07\x08\"\x96\xd6pj\x00\x00\x00j\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\x00 \x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `json` JSON NOT NULL COMMENT 'Options in JSON format.' AFTER `kind`;PK\x07\x08\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00 \x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` RENAME TO `crm_record`;\nALTER TABLE `crm_record` MODIFY COLUMN `json` json DEFAULT NULL COMMENT 'Records in JSON format.';\n\nALTER TABLE `crm_content_column` RENAME TO `crm_record_column`;\nALTER TABLE `crm_record_column` CHANGE COLUMN `content_id` `record_id` bigint(20);\n\nALTER TABLE `crm_content_links` RENAME TO `crm_record_links`;\nALTER TABLE `crm_record_links` CHANGE COLUMN `content_id` `record_id` bigint(20) unsigned;\nALTER TABLE `crm_record_links` CHANGE COLUMN `rel_content_id` `rel_record_id` bigint(20) unsigned;\nPK\x07\x08mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_chart` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the chart',\n `config` JSON NOT NULL COMMENT 'Chart & reporting configuration',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `crm_field`;\nPK\x07\x08\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x00 \x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_trigger` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the trigger',\n `enabled` BOOLEAN NOT NULL COMMENT 'Trigger enabled?',\n `actions` TEXT NOT NULL COMMENT 'All actions that trigger it',\n `source` TEXT NOT NULL COMMENT 'Trigger source',\n `rel_module` BIGINT(20) UNSIGNED NULL COMMENT 'Primary module',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\x00 \x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` DROP COLUMN `json`;\nPK\x07\x08\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008\x00 \x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8-- No more links, we'll handle this through ref field on crm_record_value tbl\nDROP TABLE IF EXISTS `crm_record_links`;\n\n-- Not columns, values\nALTER TABLE `crm_record_column` RENAME TO `crm_record_value`;\n\n-- Simplify names\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_name` `name` VARCHAR(64);\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_value` `value` TEXT;\n\n-- Add reference\nALTER TABLE `crm_record_value` ADD COLUMN `ref` BIGINT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `deleted_at` datetime DEFAULT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `place` INT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` DROP PRIMARY KEY, ADD PRIMARY KEY(`record_id`, `name`, `place`);\nCREATE INDEX crm_record_value_ref ON crm_record_value (ref);\n\n\n-- We want this as a real field\nALTER TABLE `crm_module_form` ADD COLUMN `is_multi` TINYINT(1) NOT NULL;\n\n-- This will be handled through meta(json) fieldd\nALTER TABLE `crm_module_form` DROP COLUMN `help_text`;\nALTER TABLE `crm_module_form` DROP COLUMN `max_length`;\nALTER TABLE `crm_module_form` DROP COLUMN `default_Value`;\nPK\x07\x08\x04]{\x1fo\x04\x00\x00o\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\x00 \x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` CHANGE COLUMN `user_id` `owned_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `deleted_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nUPDATE crm_record SET created_by = owned_by;\nUPDATE crm_record SET updated_by = owned_by WHERE updated_at IS NOT NULL;\nUPDATE crm_record SET deleted_by = owned_by WHERE deleted_at IS NOT NULL;\nPK\x07\x08h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE crm_attachment (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL,\n\n kind VARCHAR(32) NOT NULL,\n\n url VARCHAR(512),\n preview_url VARCHAR(512),\n\n size INT UNSIGNED,\n mimetype VARCHAR(255),\n name TEXT,\n\n meta JSON,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- page attachments will be referenced via page-block meta data\n-- module/record attachment will be referenced via crm_record_value\nPK\x07\x08\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\x00 \x0020190427180922.change-tbl-prefix.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS crm_field;\nDROP TABLE IF EXISTS crm_fields;\nDROP TABLE IF EXISTS crm_content;\nDROP TABLE IF EXISTS crm_content_links;\nDROP TABLE IF EXISTS crm_content_column;\nDROP TABLE IF EXISTS crm_module_content;\n\nALTER TABLE crm_attachment\n RENAME TO compose_attachment;\n\nALTER TABLE crm_chart\n RENAME TO compose_chart;\n\nALTER TABLE crm_module\n RENAME TO compose_module;\n\nALTER TABLE crm_module_form\n RENAME TO compose_module_form;\n\nALTER TABLE crm_page\n RENAME TO compose_page;\n\nALTER TABLE crm_record\n RENAME TO compose_record;\n\nALTER TABLE crm_record_value\n RENAME TO compose_record_value;\n\nALTER TABLE crm_trigger\n RENAME TO compose_trigger;\nPK\x07\x08\xf2\x1a)|\x97\x02\x00\x00\x97\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190427210922.namespace-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `compose_namespace` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'Name',\n `slug` VARCHAR(64) NOT NULL COMMENT 'URL slug',\n `enabled` BOOLEAN NOT NULL COMMENT 'Is namespace enabled?',\n `meta` JSON NOT NULL COMMENT 'Meta data',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08m\xeb\xed~R\x02\x00\x00R\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8INSERT IGNORE INTO compose_namespace (id, name, slug, enabled, meta)\n VALUES (88714882739863655, 'Crust CRM', 'crm', true, '{}');\n\nALTER TABLE `compose_attachment`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_chart`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_module`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_page`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_record`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_trigger`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nUPDATE `compose_attachment` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_chart` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_module` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_page` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_record` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_trigger` SET `rel_namespace` = 88714882739863655;\n\n\nALTER TABLE `compose_attachment`\n ADD CONSTRAINT `compose_attachment_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_chart`\n ADD CONSTRAINT `compose_chart_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_module`\n ADD CONSTRAINT `compose_module_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_page`\n ADD CONSTRAINT `compose_page_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_record`\n ADD CONSTRAINT `compose_record_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_trigger`\n ADD CONSTRAINT `compose_trigger_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\nPK\x07\x08\xfc\xdb\xfd\xdde \x00\x00e \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `compose_page`\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL;\n\nALTER TABLE `compose_page` CHANGE COLUMN `module_id` `rel_module` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nPK\x07\x08\x82\x01Rn1\x01\x00\x001\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190514090000.module_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE compose_module_form\n RENAME TO compose_module_field;\n\n-- Remove orphaned and invalid fields\nDELETE FROM `compose_module_field` WHERE `module_id` NOT IN (SELECT `id` FROM `compose_module`) OR `name` = '';\n\n-- Order and consistency.\nALTER TABLE `compose_module_field`\n ADD COLUMN `id` BIGINT UNSIGNED NOT NULL FIRST,\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL,\n RENAME COLUMN `module_id` TO `rel_module`,\n RENAME COLUMN `json` TO `options`;\n\n-- Generate IDs for the new field, use module, offset by one (just to start with a different ID)\n-- and use place (0 based, +1 for every field, expecting to be unique per module because of the existing pkey)\nUPDATE `compose_module_field` SET id = rel_module + 1 + place;\n\n-- Drop old primary key (module_id, place)\nALTER TABLE `compose_module_field` DROP PRIMARY KEY, ADD PRIMARY KEY(`id`);\n\n-- Foreign key\nALTER TABLE `compose_module_field`\n ADD CONSTRAINT `compose_module`\n FOREIGN KEY (`rel_module`)\n REFERENCES `compose_module` (`id`);\n\n-- And unique indexes for module+place/name combos.\nCREATE UNIQUE INDEX uid_compose_module_field_place ON compose_module_field (`rel_module`, `place`);\nCREATE UNIQUE INDEX uid_compose_module_field_name ON compose_module_field (`rel_module`, `name`);\nPK\x07\x08\xb1(\xbb\xf0\x8d\x05\x00\x00\x8d\x05\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS compose_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\nPK\x07\x08\"\xd8\xe5H\x12\x01\x00\x00\x12\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x0020190701090000.automation.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS compose_automation_trigger;\nDROP TABLE IF EXISTS compose_automation_script;\n\nCREATE TABLE IF NOT EXISTS compose_automation_script (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL DEFAULT 'unnamed' COMMENT 'The name of the script',\n `source` TEXT NOT NULL COMMENT 'Source code for the script',\n `source_ref` VARCHAR(200) NOT NULL COMMENT 'Where is the script located (if remote)',\n `async` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Do we run this script asynchronously?',\n `rel_runner` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who is running the script? 0 for invoker',\n `run_in_ua` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Run this script inside user-agent environment',\n `timeout` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Any explicit timeout set for this script (milliseconds)?',\n `critical` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is it critical that this script is executed successfully',\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is this script enabled?',\n\n `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `updated_at` DATETIME NULL DEFAULT NULL,\n `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `deleted_at` DATETIME NULL DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE IF NOT EXISTS compose_automation_trigger (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is trigger',\n\n `resource` VARCHAR(128) NOT NULL COMMENT 'Resource triggering the event',\n `event` VARCHAR(128) NOT NULL COMMENT 'Event triggered',\n `event_condition`\n TEXT NOT NULL COMMENT 'Trigger condition',\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?',\n\n `weight` INT NOT NULL DEFAULT 0,\n\n `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `updated_at` DATETIME NULL DEFAULT NULL,\n `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `deleted_at` DATETIME NULL DEFAULT NULL,\n\n CONSTRAINT `fk_script` FOREIGN KEY (`rel_script`) REFERENCES `compose_automation_script` (`id`),\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\n\n# Migrate old triggers into scripts\nINSERT INTO compose_automation_script (id, name, source, source_ref, run_in_ua, critical, enabled, created_at, updated_at, deleted_at)\nSELECT id, name, source, '', true, false, enabled, created_at, updated_at, deleted_at from compose_trigger;\n\n# Migrate old triggers into new triggers\nINSERT INTO compose_automation_trigger (id, event, resource, event_condition, rel_script, enabled, created_at, updated_at, deleted_at)\nSELECT id+seq, events.event, 'compose:record', rel_module, id, enabled, created_at, updated_at, deleted_at from compose_trigger AS t INNER JOIN\n ( SELECT 0 as seq, '' AS event\n UNION SELECT 1 as seq, 'manual' AS event\n UNION SELECT 2 as seq, 'beforeCreate' AS event\n UNION SELECT 3 as seq, 'afterCreate' AS event\n UNION SELECT 4 as seq, 'beforeUpdate' AS event\n UNION SELECT 5 as seq, 'afterUpdate' AS event\n UNION SELECT 6 as seq, 'beforeDelete' AS event\n UNION SELECT 7 as seq, 'afterDelete' AS event) AS events ON ((event = '' AND t.actions = '')\n OR (event <> '' AND t.actions LIKE concat('%',event,'%') ));\n# Normalize and cleanup\nUPDATE compose_automation_trigger SET event = 'manual' WHERE event = '';\nDELETE FROM compose_automation_trigger WHERE event_condition IN ('', '0') AND event <> 'manual';\n\nDROP TABLE IF EXISTS compose_trigger;\nPK\x07\x08\xd0\xba?\xa8\x11\x11\x00\x00\x11\x11\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x00 \x0020190825090000.automation-namespace.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `compose_automation_script`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nUPDATE `compose_automation_script` SET `rel_namespace` = (SELECT MIN(id) FROM compose_namespace);\n\nALTER TABLE `compose_automation_script`\n ADD CONSTRAINT `compose_automation_script_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nPK\x07\x08\x94\x91\x95\x83\x99\x01\x00\x00\x99\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` text NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x089S\x05%x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sql\nPK\x07\x08\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81c\n\x00\x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x84\x0c\x00\x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Q\x0d\x00\x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\\\x0e\x00\x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00D\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc8\x0f\x00\x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x005\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd4\x10\x00\x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\x96\xd6pj\x00\x00\x00j\x00\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x97\x11\x00\x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x004\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81h\x12\x00\x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x13\x00\x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc1\x15\x00\x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x18\x00\x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00&\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81r\x18\x00\x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00/\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x87\x1b\x00\x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x04]{\x1fo\x04\x00\x00o\x04\x00\x008\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x1a\x1c\x00\x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xf8 \x00\x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81w#\x00\x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\x1a)|\x97\x02\x00\x00\x97\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x81&\x00\x0020190427180922.change-tbl-prefix.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(m\xeb\xed~R\x02\x00\x00R\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81v)\x00\x0020190427210922.namespace-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xfc\xdb\xfd\xdde \x00\x00e \x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\",\x00\x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x82\x01Rn1\x01\x00\x001\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xe25\x00\x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb1(\xbb\xf0\x8d\x05\x00\x00\x8d\x05\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81o7\x00\x0020190514090000.module_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\xd8\xe5H\x12\x01\x00\x00\x12\x01\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81V=\x00\x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xd0\xba?\xa8\x11\x11\x00\x00\x11\x11\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc0>\x00\x0020190701090000.automation.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x94\x91\x95\x83\x99\x01\x00\x00\x99\x01\x00\x00*\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81(P\x00\x0020190825090000.automation-namespace.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\"R\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\xdfS\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x1b\x00\x1b\x00\xed \x00\x00KT\x00\x00\x00\x00" +var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content` (\n `id` bigint(20) unsigned NOT NULL,\n `module_id` bigint(20) unsigned NOT NULL,\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`,`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_content_column` (\n `content_id` bigint(20) NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `column_value` text NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_field` (\n `field_type` varchar(16) NOT NULL COMMENT 'Short field type (string, boolean,...)',\n `field_name` varchar(255) NOT NULL COMMENT 'Description of field contents',\n `field_template` varchar(255) NOT NULL COMMENT 'HTML template file for field',\n PRIMARY KEY (`field_type`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module` (\n `id` bigint(20) unsigned NOT NULL,\n `name` varchar(64) NOT NULL COMMENT 'The name of the module',\n `json` json NOT NULL COMMENT 'List of field definitions for the module',\n `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` datetime DEFAULT NULL,\n `deleted_at` datetime DEFAULT NULL,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_module_form` (\n `module_id` bigint(20) unsigned NOT NULL,\n `place` tinyint(3) unsigned NOT NULL,\n `kind` varchar(64) NOT NULL COMMENT 'The type of the form input field',\n `name` varchar(64) NOT NULL COMMENT 'The name of the field in the form',\n `label` varchar(255) NOT NULL COMMENT 'The label of the form input',\n `help_text` text NOT NULL COMMENT 'Help text',\n `default_value` text NOT NULL COMMENT 'Default value',\n `max_length` int(10) unsigned NOT NULL COMMENT 'Maximum input length',\n `is_private` tinyint(1) NOT NULL COMMENT 'Contains personal/sensitive data?',\n PRIMARY KEY (`module_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE `crm_page` (\n `id` bigint(20) unsigned NOT NULL COMMENT 'Page ID',\n `self_id` bigint(20) unsigned NOT NULL COMMENT 'Parent Page ID',\n `module_id` bigint(20) unsigned NOT NULL COMMENT 'Module ID (optional)',\n `title` varchar(255) NOT NULL COMMENT 'Title (required)',\n `description` text NOT NULL COMMENT 'Description',\n `blocks` json NOT NULL COMMENT 'JSON array of blocks for the page',\n `visible` tinyint(4) NOT NULL COMMENT 'Is page visible in navigation?',\n `weight` int(11) NOT NULL COMMENT 'Order for navigation',\n PRIMARY KEY (`id`) USING BTREE,\n KEY `module_id` (`module_id`),\n KEY `self_id` (`self_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` VALUES ('bool','Boolean value (yes / no)','');\nINSERT INTO `crm_field` VALUES ('email','E-mail input','');\nINSERT INTO `crm_field` VALUES ('enum','Single option picker','');\nINSERT INTO `crm_field` VALUES ('hidden','Hidden value','');\nINSERT INTO `crm_field` VALUES ('stamp','Date/time input','');\nINSERT INTO `crm_field` VALUES ('text','Text input','');\nINSERT INTO `crm_field` VALUES ('textarea','Text input (multi-line)','');\nPK\x07\x08f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00 \x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `user_id` BIGINT UNSIGNED NOT NULL AFTER `module_id`, ADD INDEX (`user_id`);\nPK\x07\x08\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8INSERT INTO `crm_field` (`field_type`, `field_name`, `field_template`) VALUES ('related', 'Related content', ''), ('related_multi', 'Related content (multiple)', '');PK\x07\x08:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_content_links` (\n `content_id` bigint(20) unsigned NOT NULL,\n `column_name` varchar(255) NOT NULL,\n `rel_content_id` bigint(20) unsigned NOT NULL,\n PRIMARY KEY (`content_id`,`column_name`,`rel_content_id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;PK\x07\x08\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00D\x00 \x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `is_required` TINYINT(1) NOT NULL AFTER `is_private`, ADD `is_visible` TINYINT(1) NOT NULL AFTER `is_required`;PK\x07\x08\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x005\x00 \x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` DROP PRIMARY KEY, ADD PRIMARY KEY(`module_id`, `place`);\nPK\x07\x08\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x00 \x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` ADD `json` json DEFAULT NULL COMMENT 'Content in JSON format.' AFTER `user_id`;\nPK\x07\x08\"\x96\xd6pj\x00\x00\x00j\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\x00 \x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_module_form` ADD `json` JSON NOT NULL COMMENT 'Options in JSON format.' AFTER `kind`;PK\x07\x08\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x00 \x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_content` RENAME TO `crm_record`;\nALTER TABLE `crm_record` MODIFY COLUMN `json` json DEFAULT NULL COMMENT 'Records in JSON format.';\n\nALTER TABLE `crm_content_column` RENAME TO `crm_record_column`;\nALTER TABLE `crm_record_column` CHANGE COLUMN `content_id` `record_id` bigint(20);\n\nALTER TABLE `crm_content_links` RENAME TO `crm_record_links`;\nALTER TABLE `crm_record_links` CHANGE COLUMN `content_id` `record_id` bigint(20) unsigned;\nALTER TABLE `crm_record_links` CHANGE COLUMN `rel_content_id` `rel_record_id` bigint(20) unsigned;\nPK\x07\x08mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_chart` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the chart',\n `config` JSON NOT NULL COMMENT 'Chart & reporting configuration',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE `crm_field`;\nPK\x07\x08\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x00 \x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `crm_trigger` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'The name of the trigger',\n `enabled` BOOLEAN NOT NULL COMMENT 'Trigger enabled?',\n `actions` TEXT NOT NULL COMMENT 'All actions that trigger it',\n `source` TEXT NOT NULL COMMENT 'Trigger source',\n `rel_module` BIGINT(20) UNSIGNED NULL COMMENT 'Primary module',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/\x00 \x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` DROP COLUMN `json`;\nPK\x07\x08\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008\x00 \x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8-- No more links, we'll handle this through ref field on crm_record_value tbl\nDROP TABLE IF EXISTS `crm_record_links`;\n\n-- Not columns, values\nALTER TABLE `crm_record_column` RENAME TO `crm_record_value`;\n\n-- Simplify names\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_name` `name` VARCHAR(64);\nALTER TABLE `crm_record_value` CHANGE COLUMN `column_value` `value` TEXT;\n\n-- Add reference\nALTER TABLE `crm_record_value` ADD COLUMN `ref` BIGINT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `deleted_at` datetime DEFAULT NULL;\nALTER TABLE `crm_record_value` ADD COLUMN `place` INT UNSIGNED DEFAULT 0 NOT NULL;\nALTER TABLE `crm_record_value` DROP PRIMARY KEY, ADD PRIMARY KEY(`record_id`, `name`, `place`);\nCREATE INDEX crm_record_value_ref ON crm_record_value (ref);\n\n\n-- We want this as a real field\nALTER TABLE `crm_module_form` ADD COLUMN `is_multi` TINYINT(1) NOT NULL;\n\n-- This will be handled through meta(json) fieldd\nALTER TABLE `crm_module_form` DROP COLUMN `help_text`;\nALTER TABLE `crm_module_form` DROP COLUMN `max_length`;\nALTER TABLE `crm_module_form` DROP COLUMN `default_Value`;\nPK\x07\x08\x04]{\x1fo\x04\x00\x00o\x04\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\x00 \x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `crm_record` CHANGE COLUMN `user_id` `owned_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `created_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `updated_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nALTER TABLE `crm_record` ADD COLUMN `deleted_by` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nUPDATE crm_record SET created_by = owned_by;\nUPDATE crm_record SET updated_by = owned_by WHERE updated_at IS NOT NULL;\nUPDATE crm_record SET deleted_by = owned_by WHERE deleted_at IS NOT NULL;\nPK\x07\x08h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE crm_attachment (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL,\n\n kind VARCHAR(32) NOT NULL,\n\n url VARCHAR(512),\n preview_url VARCHAR(512),\n\n size INT UNSIGNED,\n mimetype VARCHAR(255),\n name TEXT,\n\n meta JSON,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL,\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- page attachments will be referenced via page-block meta data\n-- module/record attachment will be referenced via crm_record_value\nPK\x07\x08\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\x00 \x0020190427180922.change-tbl-prefix.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS crm_field;\nDROP TABLE IF EXISTS crm_fields;\nDROP TABLE IF EXISTS crm_content;\nDROP TABLE IF EXISTS crm_content_links;\nDROP TABLE IF EXISTS crm_content_column;\nDROP TABLE IF EXISTS crm_module_content;\n\nALTER TABLE crm_attachment\n RENAME TO compose_attachment;\n\nALTER TABLE crm_chart\n RENAME TO compose_chart;\n\nALTER TABLE crm_module\n RENAME TO compose_module;\n\nALTER TABLE crm_module_form\n RENAME TO compose_module_form;\n\nALTER TABLE crm_page\n RENAME TO compose_page;\n\nALTER TABLE crm_record\n RENAME TO compose_record;\n\nALTER TABLE crm_record_value\n RENAME TO compose_record_value;\n\nALTER TABLE crm_trigger\n RENAME TO compose_trigger;\nPK\x07\x08\xf2\x1a)|\x97\x02\x00\x00\x97\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190427210922.namespace-tbl.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `compose_namespace` (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL COMMENT 'Name',\n `slug` VARCHAR(64) NOT NULL COMMENT 'URL slug',\n `enabled` BOOLEAN NOT NULL COMMENT 'Is namespace enabled?',\n `meta` JSON NOT NULL COMMENT 'Meta data',\n\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_at` DATETIME DEFAULT NULL,\n `deleted_at` DATETIME DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08m\xeb\xed~R\x02\x00\x00R\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00 \x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8INSERT IGNORE INTO compose_namespace (id, name, slug, enabled, meta)\n VALUES (88714882739863655, 'Crust CRM', 'crm', true, '{}');\n\nALTER TABLE `compose_attachment`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_chart`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_module`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_page`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_record`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nALTER TABLE `compose_trigger`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nUPDATE `compose_attachment` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_chart` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_module` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_page` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_record` SET `rel_namespace` = 88714882739863655;\nUPDATE `compose_trigger` SET `rel_namespace` = 88714882739863655;\n\n\nALTER TABLE `compose_attachment`\n ADD CONSTRAINT `compose_attachment_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_chart`\n ADD CONSTRAINT `compose_chart_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_module`\n ADD CONSTRAINT `compose_module_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_page`\n ADD CONSTRAINT `compose_page_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_record`\n ADD CONSTRAINT `compose_record_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nALTER TABLE `compose_trigger`\n ADD CONSTRAINT `compose_trigger_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\nPK\x07\x08\xfc\xdb\xfd\xdde \x00\x00e \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00%\x00 \x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `compose_page`\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL;\n\nALTER TABLE `compose_page` CHANGE COLUMN `module_id` `rel_module` BIGINT UNSIGNED NOT NULL DEFAULT 0;\nPK\x07\x08\x82\x01Rn1\x01\x00\x001\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190514090000.module_fields.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE compose_module_form\n RENAME TO compose_module_field;\n\n-- Remove orphaned and invalid fields\nDELETE FROM `compose_module_field` WHERE `module_id` NOT IN (SELECT `id` FROM `compose_module`) OR `name` = '';\n\n-- Order and consistency.\nALTER TABLE `compose_module_field`\n ADD COLUMN `id` BIGINT UNSIGNED NOT NULL FIRST,\n ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n ADD COLUMN `updated_at` DATETIME DEFAULT NULL,\n ADD COLUMN `deleted_at` DATETIME DEFAULT NULL,\n RENAME COLUMN `module_id` TO `rel_module`,\n RENAME COLUMN `json` TO `options`;\n\n-- Generate IDs for the new field, use module, offset by one (just to start with a different ID)\n-- and use place (0 based, +1 for every field, expecting to be unique per module because of the existing pkey)\nUPDATE `compose_module_field` SET id = rel_module + 1 + place;\n\n-- Drop old primary key (module_id, place)\nALTER TABLE `compose_module_field` DROP PRIMARY KEY, ADD PRIMARY KEY(`id`);\n\n-- Foreign key\nALTER TABLE `compose_module_field`\n ADD CONSTRAINT `compose_module`\n FOREIGN KEY (`rel_module`)\n REFERENCES `compose_module` (`id`);\n\n-- And unique indexes for module+place/name combos.\nCREATE UNIQUE INDEX uid_compose_module_field_place ON compose_module_field (`rel_module`, `place`);\nCREATE UNIQUE INDEX uid_compose_module_field_name ON compose_module_field (`rel_module`, `name`);\nPK\x07\x08\xb1(\xbb\xf0\x8d\x05\x00\x00\x8d\x05\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS compose_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\nPK\x07\x08\"\xd8\xe5H\x12\x01\x00\x00\x12\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x0020190701090000.automation.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS compose_automation_trigger;\nDROP TABLE IF EXISTS compose_automation_script;\n\nCREATE TABLE IF NOT EXISTS compose_automation_script (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL DEFAULT 'unnamed' COMMENT 'The name of the script',\n `source` TEXT NOT NULL COMMENT 'Source code for the script',\n `source_ref` VARCHAR(200) NOT NULL COMMENT 'Where is the script located (if remote)',\n `async` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Do we run this script asynchronously?',\n `rel_runner` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who is running the script? 0 for invoker',\n `run_in_ua` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Run this script inside user-agent environment',\n `timeout` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Any explicit timeout set for this script (milliseconds)?',\n `critical` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is it critical that this script is executed successfully',\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is this script enabled?',\n\n `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `updated_at` DATETIME NULL DEFAULT NULL,\n `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `deleted_at` DATETIME NULL DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE IF NOT EXISTS compose_automation_trigger (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is triggered',\n\n `resource` VARCHAR(128) NOT NULL COMMENT 'Resource triggering the event',\n `event` VARCHAR(128) NOT NULL COMMENT 'Event triggered',\n `event_condition`\n TEXT NOT NULL COMMENT 'Trigger condition',\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?',\n\n `weight` INT NOT NULL DEFAULT 0,\n\n `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `updated_at` DATETIME NULL DEFAULT NULL,\n `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `deleted_at` DATETIME NULL DEFAULT NULL,\n\n CONSTRAINT `fk_script` FOREIGN KEY (`rel_script`) REFERENCES `compose_automation_script` (`id`),\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\n\n# Migrate old triggers into scripts\nINSERT INTO compose_automation_script (id, name, source, source_ref, run_in_ua, critical, enabled, created_at, updated_at, deleted_at)\nSELECT id, name, source, '', true, false, enabled, created_at, updated_at, deleted_at from compose_trigger;\n\n# Migrate old triggers into new triggers\nINSERT INTO compose_automation_trigger (id, event, resource, event_condition, rel_script, enabled, created_at, updated_at, deleted_at)\nSELECT id+seq, events.event, 'compose:record', rel_module, id, enabled, created_at, updated_at, deleted_at from compose_trigger AS t INNER JOIN\n ( SELECT 0 as seq, '' AS event\n UNION SELECT 1 as seq, 'manual' AS event\n UNION SELECT 2 as seq, 'beforeCreate' AS event\n UNION SELECT 3 as seq, 'afterCreate' AS event\n UNION SELECT 4 as seq, 'beforeUpdate' AS event\n UNION SELECT 5 as seq, 'afterUpdate' AS event\n UNION SELECT 6 as seq, 'beforeDelete' AS event\n UNION SELECT 7 as seq, 'afterDelete' AS event) AS events ON ((event = '' AND t.actions = '')\n OR (event <> '' AND t.actions LIKE concat('%',event,'%') ));\n# Normalize and cleanup\nUPDATE compose_automation_trigger SET event = 'manual' WHERE event = '';\nDELETE FROM compose_automation_trigger WHERE event_condition IN ('', '0') AND event <> 'manual';\n\nDROP TABLE IF EXISTS compose_trigger;\nPK\x07\x08c\xda\x17\xa4\x13\x11\x00\x00\x13\x11\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x00 \x0020190825090000.automation-namespace.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `compose_automation_script`\n ADD `rel_namespace` BIGINT UNSIGNED NOT NULL AFTER `id`,\n ADD INDEX (`rel_namespace`);\n\nUPDATE `compose_automation_script` SET `rel_namespace` = (SELECT MIN(id) FROM compose_namespace);\n\nALTER TABLE `compose_automation_script`\n ADD CONSTRAINT `compose_automation_script_namespace`\n FOREIGN KEY (`rel_namespace`)\n REFERENCES `compose_namespace` (`id`);\n\nPK\x07\x08\x94\x91\x95\x83\x99\x01\x00\x00\x99\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` text NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x089S\x05%x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sql\nPK\x07\x08\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xac\xe8\x19\x1d\x12\n\x00\x00\x12\n\x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x18\x1e\x84\xc5\x01\x00\x00\xc5\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81c\n\x00\x0020180704080001.crm_fields-data.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xeb!\x81\xc2k\x00\x00\x00k\x00\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x84\x0c\x00\x0020181109133134.crm_content-ownership.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(:.\xfb8\xa6\x00\x00\x00\xa6\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Q\x0d\x00\x0020181109193047.crm_fields-related_types.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xee\x12\x15 \x05\x01\x00\x00\x05\x01\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\\\x0e\x00\x0020181125122152.add_multiple_relationships.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xa5q c\x91\x00\x00\x00\x91\x00\x00\x00D\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc8\x0f\x00\x0020181125132142.add_required_and_visible_to_module_form_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xd9\xd4i\xe3W\x00\x00\x00W\x00\x00\x005\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd4\x10\x00\x0020181202163130.fix-crm-module-form-primary-key.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\x96\xd6pj\x00\x00\x00j\x00\x00\x000\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x97\x11\x00\x0020181204123650.add-crm-content-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb7\x93\xd4\xf6f\x00\x00\x00f\x00\x00\x004\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81h\x12\x00\x0020181204155326.add-crm-module-form-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(mA\xa8\x1e&\x02\x00\x00&\x02\x00\x00+\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x13\x00\x0020181216214630.crm-content-to-record.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xcf\xc6g\xf6\xe4\x01\x00\x00\xe4\x01\x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc1\x15\x00\x0020181217100000.add-charts-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xae \xfd2\x18\x00\x00\x00\x18\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x18\x00\x0020181224122301.rem-crm_field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(+\xad\xb7\xed\xb8\x02\x00\x00\xb8\x02\x00\x00&\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81r\x18\x00\x0020190108100000.add-triggers-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x94#\xb9\x99-\x00\x00\x00-\x00\x00\x00/\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x87\x1b\x00\x0020190110175924.rem-crm-record-json-field.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x04]{\x1fo\x04\x00\x00o\x04\x00\x008\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x1a\x1c\x00\x0020190114072000.cleanup-record-tables-and-multival.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(h\xe2\xeb\n!\x02\x00\x00!\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xf8 \x00\x0020190121132408.record-updated-by.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xde?\x08\xb3\x02\x00\x00\xb3\x02\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81w#\x00\x0020190227090642.attachment.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\x1a)|\x97\x02\x00\x00\x97\x02\x00\x00'\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x81&\x00\x0020190427180922.change-tbl-prefix.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(m\xeb\xed~R\x02\x00\x00R\x02\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81v)\x00\x0020190427210922.namespace-tbl.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xfc\xdb\xfd\xdde \x00\x00e \x00\x00$\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\",\x00\x0020190428080000.namespace-refs.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x82\x01Rn1\x01\x00\x001\x01\x00\x00%\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xe25\x00\x0020190428080000.page-timestamps.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xb1(\xbb\xf0\x8d\x05\x00\x00\x8d\x05\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81o7\x00\x0020190514090000.module_fields.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\"\xd8\xe5H\x12\x01\x00\x00\x12\x01\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81V=\x00\x0020190526090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(c\xda\x17\xa4\x13\x11\x00\x00\x13\x11\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xc0>\x00\x0020190701090000.automation.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x94\x91\x95\x83\x99\x01\x00\x00\x99\x01\x00\x00*\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81*P\x00\x0020190825090000.automation-namespace.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9S\x05%x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81$R\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xc1h\xf1\xfb/\x00\x00\x00/\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\xe1S\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x1b\x00\x1b\x00\xed \x00\x00MT\x00\x00\x00\x00" diff --git a/compose/db/schema/mysql/20190701090000.automation.up.sql b/compose/db/schema/mysql/20190701090000.automation.up.sql index c2999a5e8..27fd33448 100644 --- a/compose/db/schema/mysql/20190701090000.automation.up.sql +++ b/compose/db/schema/mysql/20190701090000.automation.up.sql @@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS compose_automation_script ( CREATE TABLE IF NOT EXISTS compose_automation_trigger ( `id` BIGINT(20) UNSIGNED NOT NULL, - `rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is trigger', + `rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is triggered', `resource` VARCHAR(128) NOT NULL COMMENT 'Resource triggering the event', `event` VARCHAR(128) NOT NULL COMMENT 'Event triggered', diff --git a/compose/internal/service/service.go b/compose/internal/service/service.go index 50f3932bc..c3d1523a4 100644 --- a/compose/internal/service/service.go +++ b/compose/internal/service/service.go @@ -32,7 +32,7 @@ type ( var ( DefaultLogger *zap.Logger - // DefaultPermissions Retrives & stores permissions + // DefaultPermissions Retrieves & stores permissions DefaultPermissions permissionServicer // DefaultAccessControl Access control checking diff --git a/compose/rest/automation_trigger.go b/compose/rest/automation_trigger.go index 1eac6bb78..34c74d77d 100644 --- a/compose/rest/automation_trigger.go +++ b/compose/rest/automation_trigger.go @@ -63,10 +63,6 @@ func (AutomationTrigger) New() *AutomationTrigger { func (ctrl AutomationTrigger) List(ctx context.Context, r *request.AutomationTriggerList) (interface{}, error) { set, filter, err := ctrl.triggers.Find(ctx, automation.TriggerFilter{ - // @todo namespace filtering - // Might be a bit tricky as triggers themselves not know about namespaces - // Namespace: r.NamespaceID - Resource: r.Resource, Event: r.Event, ScriptID: r.ScriptID, diff --git a/compose/rest/router.go b/compose/rest/router.go index e11351f4e..1c4a8f655 100644 --- a/compose/rest/router.go +++ b/compose/rest/router.go @@ -17,8 +17,8 @@ func MountRoutes(r chi.Router) { notification = Notification{}.New() attachment = Attachment{}.New() - automationScript = AutomationScript{}.New() - automationTrgger = AutomationTrigger{}.New() + automationScript = AutomationScript{}.New() + automationTrigger = AutomationTrigger{}.New() ) // Initialize handlers & controllers. @@ -41,6 +41,6 @@ func MountRoutes(r chi.Router) { handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) handlers.NewAutomationScript(automationScript).MountRoutes(r) - handlers.NewAutomationTrigger(automationTrgger).MountRoutes(r) + handlers.NewAutomationTrigger(automationTrigger).MountRoutes(r) }) } diff --git a/docs/system/README.md b/docs/system/README.md index 7f7814d02..6989a03d2 100644 --- a/docs/system/README.md +++ b/docs/system/README.md @@ -278,6 +278,229 @@ +# Automation scripts + +| Method | Endpoint | Purpose | +| ------ | -------- | ------- | +| `GET` | `/automation/script/` | List/read automation script | +| `POST` | `/automation/script/` | Add new automation script | +| `GET` | `/automation/script/{scriptID}` | Read automation script by ID | +| `POST` | `/automation/script/{scriptID}` | Update automation script | +| `DELETE` | `/automation/script/{scriptID}` | Delete script | +| `POST` | `/automation/script/test` | Run source code in corredor. Used for testing | + +## List/read automation script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| query | string | GET | Search query to match against automation script | N/A | NO | +| resource | string | GET | Limit by resource (via trigger) | N/A | NO | +| incDeleted | bool | GET | Include deleted scripts | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | + +## Add new automation script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| name | string | POST | automation name | N/A | NO | +| sourceRef | string | POST | Source URL | N/A | NO | +| source | string | POST | Source code | N/A | NO | +| runAs | uint64 | POST | Run as specific user | N/A | NO | +| timeout | uint | POST | Script timeout (in milliseconds) | N/A | NO | +| critical | bool | POST | Is it critical to run this script successfully | N/A | NO | +| async | bool | POST | Will this script be ran asynchronously | N/A | NO | +| enabled | bool | POST | | N/A | NO | +| triggers | automation.TriggerSet | POST | | N/A | NO | + +## Read automation script by ID + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/{scriptID}` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| scriptID | uint64 | PATH | automation script ID | N/A | YES | + +## Update automation script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/{scriptID}` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| scriptID | uint64 | PATH | Automation script ID | N/A | YES | +| name | string | POST | Script name | N/A | NO | +| sourceRef | string | POST | Source URL | N/A | NO | +| source | string | POST | Source code | N/A | NO | +| runAs | uint64 | POST | Run script as specific user | N/A | NO | +| timeout | uint | POST | Run script in user-agent (browser) | N/A | NO | +| critical | bool | POST | Is it critical to run this script successfully | N/A | NO | +| async | bool | POST | Will this script be ran asynchronously | N/A | NO | +| enabled | bool | POST | | N/A | NO | +| triggers | automation.TriggerSet | POST | | N/A | NO | + +## Delete script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/{scriptID}` | HTTP/S | DELETE | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +## Run source code in corredor. Used for testing + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/test` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| source | string | POST | Script's source code | N/A | NO | +| payload | json.RawMessage | POST | Payload to be used | N/A | NO | + +--- + + + + +# Automation script triggers + +| Method | Endpoint | Purpose | +| ------ | -------- | ------- | +| `GET` | `/automation/script/{scriptID}/trigger/` | List/read automation script triggers | +| `POST` | `/automation/script/{scriptID}/trigger/` | Add new automation script trigger | +| `GET` | `/automation/script/{scriptID}/trigger/{triggerID}` | Read automation script trigger by ID | +| `POST` | `/automation/script/{scriptID}/trigger/{triggerID}` | Update automation script trigger | +| `DELETE` | `/automation/script/{scriptID}/trigger/{triggerID}` | Delete script | + +## List/read automation script triggers + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/{scriptID}/trigger/` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| resource | string | GET | Only triggers of a specific resource | N/A | NO | +| event | string | GET | Only triggers of a specific event | N/A | NO | +| incDeleted | bool | GET | Include deleted scripts | N/A | NO | +| page | uint | GET | Page number (0 based) | N/A | NO | +| perPage | uint | GET | Returned items per page (default 50) | N/A | NO | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +## Add new automation script trigger + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/{scriptID}/trigger/` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| resource | string | POST | Resource | N/A | YES | +| event | string | POST | Event | N/A | YES | +| condition | string | POST | Event | N/A | NO | +| enabled | bool | POST | | N/A | NO | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +## Read automation script trigger by ID + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | GET | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| triggerID | uint64 | PATH | Automation script trigger ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +## Update automation script trigger + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | POST | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| triggerID | uint64 | PATH | Automation script trigger ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | +| resource | string | POST | Resource | N/A | YES | +| event | string | POST | Event | N/A | YES | +| condition | string | POST | Event | N/A | NO | +| enabled | bool | POST | | N/A | NO | + +## Delete script + +#### Method + +| URI | Protocol | Method | Authentication | +| --- | -------- | ------ | -------------- | +| `/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | DELETE | Client ID, Session ID | + +#### Request parameters + +| Parameter | Type | Method | Description | Default | Required? | +| --------- | ---- | ------ | ----------- | ------- | --------- | +| triggerID | uint64 | PATH | Automation script trigger ID | N/A | YES | +| scriptID | uint64 | PATH | Script ID | N/A | YES | + +--- + + + + # Organisations Organisations represent a top-level grouping entity. There may be many organisations defined in a single deployment. diff --git a/pkg/automation/corredor/service-corredor.pb.go b/pkg/automation/corredor/service-corredor.pb.go index 16dbdab67..7a5771197 100644 --- a/pkg/automation/corredor/service-corredor.pb.go +++ b/pkg/automation/corredor/service-corredor.pb.go @@ -7,6 +7,7 @@ import ( context "context" fmt "fmt" proto1 "github.com/cortezaproject/corteza-server/compose/proto" + proto2 "github.com/cortezaproject/corteza-server/system/proto" proto "github.com/golang/protobuf/proto" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" @@ -261,6 +262,61 @@ func (m *RunRecordRequest) GetRecord() *proto1.Record { return nil } +type RunMailMessageRequest struct { + Config map[string]string `protobuf:"bytes,1,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Script *Script `protobuf:"bytes,2,opt,name=script,proto3" json:"script,omitempty"` + MailMessage *proto2.MailMessage `protobuf:"bytes,3,opt,name=mailMessage,proto3" json:"mailMessage,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RunMailMessageRequest) Reset() { *m = RunMailMessageRequest{} } +func (m *RunMailMessageRequest) String() string { return proto.CompactTextString(m) } +func (*RunMailMessageRequest) ProtoMessage() {} +func (*RunMailMessageRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_c06bb92bf45e37e2, []int{4} +} + +func (m *RunMailMessageRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RunMailMessageRequest.Unmarshal(m, b) +} +func (m *RunMailMessageRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RunMailMessageRequest.Marshal(b, m, deterministic) +} +func (m *RunMailMessageRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_RunMailMessageRequest.Merge(m, src) +} +func (m *RunMailMessageRequest) XXX_Size() int { + return xxx_messageInfo_RunMailMessageRequest.Size(m) +} +func (m *RunMailMessageRequest) XXX_DiscardUnknown() { + xxx_messageInfo_RunMailMessageRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_RunMailMessageRequest proto.InternalMessageInfo + +func (m *RunMailMessageRequest) GetConfig() map[string]string { + if m != nil { + return m.Config + } + return nil +} + +func (m *RunMailMessageRequest) GetScript() *Script { + if m != nil { + return m.Script + } + return nil +} + +func (m *RunMailMessageRequest) GetMailMessage() *proto2.MailMessage { + if m != nil { + return m.MailMessage + } + return nil +} + type RunTestResponse struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -271,7 +327,7 @@ func (m *RunTestResponse) Reset() { *m = RunTestResponse{} } func (m *RunTestResponse) String() string { return proto.CompactTextString(m) } func (*RunTestResponse) ProtoMessage() {} func (*RunTestResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_c06bb92bf45e37e2, []int{4} + return fileDescriptor_c06bb92bf45e37e2, []int{5} } func (m *RunTestResponse) XXX_Unmarshal(b []byte) error { @@ -303,7 +359,7 @@ func (m *RunNamespaceResponse) Reset() { *m = RunNamespaceResponse{} } func (m *RunNamespaceResponse) String() string { return proto.CompactTextString(m) } func (*RunNamespaceResponse) ProtoMessage() {} func (*RunNamespaceResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_c06bb92bf45e37e2, []int{5} + return fileDescriptor_c06bb92bf45e37e2, []int{6} } func (m *RunNamespaceResponse) XXX_Unmarshal(b []byte) error { @@ -342,7 +398,7 @@ func (m *RunModuleResponse) Reset() { *m = RunModuleResponse{} } func (m *RunModuleResponse) String() string { return proto.CompactTextString(m) } func (*RunModuleResponse) ProtoMessage() {} func (*RunModuleResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_c06bb92bf45e37e2, []int{6} + return fileDescriptor_c06bb92bf45e37e2, []int{7} } func (m *RunModuleResponse) XXX_Unmarshal(b []byte) error { @@ -381,7 +437,7 @@ func (m *RunRecordResponse) Reset() { *m = RunRecordResponse{} } func (m *RunRecordResponse) String() string { return proto.CompactTextString(m) } func (*RunRecordResponse) ProtoMessage() {} func (*RunRecordResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_c06bb92bf45e37e2, []int{7} + return fileDescriptor_c06bb92bf45e37e2, []int{8} } func (m *RunRecordResponse) XXX_Unmarshal(b []byte) error { @@ -409,6 +465,37 @@ func (m *RunRecordResponse) GetRecord() *proto1.Record { return nil } +type RunMailMessageResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RunMailMessageResponse) Reset() { *m = RunMailMessageResponse{} } +func (m *RunMailMessageResponse) String() string { return proto.CompactTextString(m) } +func (*RunMailMessageResponse) ProtoMessage() {} +func (*RunMailMessageResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_c06bb92bf45e37e2, []int{9} +} + +func (m *RunMailMessageResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RunMailMessageResponse.Unmarshal(m, b) +} +func (m *RunMailMessageResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RunMailMessageResponse.Marshal(b, m, deterministic) +} +func (m *RunMailMessageResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_RunMailMessageResponse.Merge(m, src) +} +func (m *RunMailMessageResponse) XXX_Size() int { + return xxx_messageInfo_RunMailMessageResponse.Size(m) +} +func (m *RunMailMessageResponse) XXX_DiscardUnknown() { + xxx_messageInfo_RunMailMessageResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_RunMailMessageResponse proto.InternalMessageInfo + type Script struct { Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` @@ -423,7 +510,7 @@ func (m *Script) Reset() { *m = Script{} } func (m *Script) String() string { return proto.CompactTextString(m) } func (*Script) ProtoMessage() {} func (*Script) Descriptor() ([]byte, []int) { - return fileDescriptor_c06bb92bf45e37e2, []int{8} + return fileDescriptor_c06bb92bf45e37e2, []int{10} } func (m *Script) XXX_Unmarshal(b []byte) error { @@ -480,48 +567,56 @@ func init() { proto.RegisterMapType((map[string]string)(nil), "corredor.RunModuleRequest.ConfigEntry") proto.RegisterType((*RunRecordRequest)(nil), "corredor.RunRecordRequest") proto.RegisterMapType((map[string]string)(nil), "corredor.RunRecordRequest.ConfigEntry") + proto.RegisterType((*RunMailMessageRequest)(nil), "corredor.RunMailMessageRequest") + proto.RegisterMapType((map[string]string)(nil), "corredor.RunMailMessageRequest.ConfigEntry") proto.RegisterType((*RunTestResponse)(nil), "corredor.RunTestResponse") proto.RegisterType((*RunNamespaceResponse)(nil), "corredor.RunNamespaceResponse") proto.RegisterType((*RunModuleResponse)(nil), "corredor.RunModuleResponse") proto.RegisterType((*RunRecordResponse)(nil), "corredor.RunRecordResponse") + proto.RegisterType((*RunMailMessageResponse)(nil), "corredor.RunMailMessageResponse") proto.RegisterType((*Script)(nil), "corredor.Script") } func init() { proto.RegisterFile("service-corredor.proto", fileDescriptor_c06bb92bf45e37e2) } var fileDescriptor_c06bb92bf45e37e2 = []byte{ - // 492 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x54, 0xdd, 0x8a, 0x13, 0x31, - 0x14, 0x26, 0x6d, 0x77, 0xdc, 0x9e, 0xaa, 0xdb, 0x8d, 0x65, 0x8d, 0x23, 0xca, 0x32, 0x17, 0x5a, - 0x2f, 0x1c, 0xa5, 0xde, 0xf8, 0xb3, 0x08, 0x8b, 0x08, 0x22, 0xe8, 0x45, 0xf4, 0x05, 0x6a, 0x1a, - 0xa5, 0xb8, 0x93, 0x8c, 0xc9, 0x64, 0xa1, 0xaf, 0xe1, 0xbd, 0x4f, 0xe2, 0x43, 0xf9, 0x0a, 0x32, - 0xf9, 0x99, 0x69, 0xc6, 0x6e, 0x59, 0x29, 0x88, 0x77, 0x73, 0xce, 0xf9, 0x72, 0xce, 0x7c, 0xdf, - 0xf9, 0x81, 0x23, 0xcd, 0xd5, 0xf9, 0x92, 0xf1, 0x87, 0x4c, 0x2a, 0xc5, 0x17, 0x52, 0xe5, 0xa5, - 0x92, 0x95, 0xc4, 0xfb, 0xc1, 0x4e, 0x6f, 0x32, 0x59, 0x94, 0x52, 0xf3, 0x47, 0x62, 0x5e, 0x70, - 0x5d, 0xce, 0x19, 0x77, 0x90, 0x74, 0x12, 0x02, 0x85, 0x5c, 0x98, 0xb3, 0x3f, 0xbc, 0x8a, 0x33, - 0xa9, 0x16, 0xce, 0x9b, 0x9d, 0xc0, 0x75, 0x6a, 0xc4, 0x47, 0xae, 0x2b, 0xca, 0xbf, 0x19, 0xae, - 0x2b, 0x7c, 0x04, 0x89, 0x96, 0x46, 0x31, 0x4e, 0xd0, 0x31, 0x9a, 0x0e, 0xa9, 0xb7, 0x30, 0x86, - 0x41, 0x5d, 0x88, 0xf4, 0xac, 0xd7, 0x7e, 0x67, 0xbf, 0x10, 0xdc, 0xa0, 0x46, 0xbc, 0x0f, 0x3f, - 0x10, 0x72, 0x9c, 0x42, 0xc2, 0xa4, 0xf8, 0xbc, 0xfc, 0x42, 0xd0, 0x71, 0x7f, 0x3a, 0x9a, 0x3d, - 0xc8, 0x1b, 0x16, 0x1b, 0xe0, 0xf9, 0x2b, 0x8b, 0x7d, 0x2d, 0x2a, 0xb5, 0xa2, 0xfe, 0x21, 0x9e, - 0x42, 0xa2, 0x99, 0x5a, 0x96, 0x95, 0x2d, 0x38, 0x9a, 0x8d, 0xdb, 0x14, 0x1f, 0xac, 0x9f, 0xfa, - 0x38, 0x7e, 0x0c, 0xc3, 0x46, 0x01, 0xd2, 0xb7, 0x60, 0x9c, 0x7b, 0xb2, 0x79, 0x5b, 0xab, 0x05, - 0xa5, 0xcf, 0x60, 0xb4, 0x56, 0x12, 0x8f, 0xa1, 0xff, 0x95, 0xaf, 0x3c, 0xdd, 0xfa, 0x13, 0x4f, - 0x60, 0xef, 0x7c, 0x7e, 0x66, 0x02, 0x59, 0x67, 0x3c, 0xef, 0x3d, 0x45, 0xd9, 0xf7, 0x1e, 0x8c, - 0xa9, 0x11, 0xef, 0xac, 0xb2, 0x81, 0xee, 0xcb, 0x0e, 0xdd, 0x7b, 0x11, 0xdd, 0x08, 0xfb, 0xaf, - 0xb9, 0xe2, 0xfb, 0x90, 0xb8, 0x31, 0x20, 0x03, 0x0b, 0x3f, 0x68, 0xe0, 0xfe, 0xbf, 0x7c, 0x78, - 0x17, 0x51, 0x7e, 0x3a, 0x51, 0xa8, 0x1d, 0xac, 0xcb, 0x89, 0x12, 0x61, 0xff, 0x5b, 0x51, 0x6a, - 0xa0, 0x5b, 0x17, 0xb2, 0xd7, 0x01, 0x7a, 0x02, 0x3e, 0xbc, 0x8b, 0x7a, 0x87, 0x70, 0xd0, 0xac, - 0xa0, 0x2e, 0xa5, 0xd0, 0x3c, 0x7b, 0x03, 0x93, 0x78, 0x4f, 0x9c, 0x3f, 0x66, 0x8a, 0x2e, 0xc1, - 0x34, 0x3b, 0x81, 0xc3, 0xb5, 0x11, 0xf4, 0x69, 0x5a, 0xfa, 0x68, 0x2b, 0x7d, 0xff, 0x3a, 0xf4, - 0xaa, 0x7d, 0xed, 0x35, 0x41, 0x5b, 0x35, 0xc9, 0x16, 0x90, 0xb8, 0x4e, 0xfd, 0xcd, 0x4d, 0xc1, - 0x04, 0xae, 0x54, 0xcb, 0x82, 0x4b, 0x53, 0xd9, 0x5e, 0x5e, 0xa3, 0xc1, 0xac, 0x25, 0x9c, 0xeb, - 0x95, 0x60, 0xb6, 0x69, 0xfb, 0xd4, 0x19, 0xb3, 0x1f, 0x3d, 0xb8, 0xea, 0x07, 0xc2, 0x08, 0xc1, - 0x15, 0x7e, 0x01, 0x83, 0x5a, 0x4c, 0x4c, 0xa2, 0x81, 0x5b, 0x3b, 0x71, 0xe9, 0xad, 0x0d, 0x11, - 0x4f, 0xee, 0x2d, 0x0c, 0x1b, 0x1d, 0xf1, 0x9d, 0xad, 0x67, 0x2b, 0xbd, 0x7b, 0x51, 0xd8, 0xe7, - 0x3a, 0x85, 0xc4, 0xe9, 0x89, 0xd3, 0x8b, 0x0f, 0x42, 0x7a, 0x7b, 0x63, 0xac, 0x4d, 0xe1, 0x44, - 0xed, 0xa4, 0x88, 0xd6, 0xa7, 0x93, 0x22, 0x6e, 0xd7, 0xa7, 0xc4, 0x1e, 0xfa, 0x27, 0xbf, 0x03, - 0x00, 0x00, 0xff, 0xff, 0x72, 0x3d, 0x15, 0x27, 0x51, 0x06, 0x00, 0x00, + // 576 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x55, 0x5f, 0x6b, 0x13, 0x41, + 0x10, 0xe7, 0x92, 0xf4, 0x6c, 0x26, 0x6a, 0xd3, 0x6d, 0x8c, 0xd7, 0x13, 0x35, 0xdc, 0x83, 0x46, + 0xc4, 0xab, 0x44, 0x04, 0xff, 0x14, 0xa1, 0x14, 0x41, 0x84, 0x8a, 0xac, 0xbe, 0xcb, 0x79, 0x59, + 0xcb, 0x61, 0x6e, 0x37, 0xee, 0xde, 0x15, 0xf2, 0x35, 0xfc, 0x3a, 0x7e, 0xa8, 0xbe, 0xfa, 0x28, + 0xb7, 0x3b, 0xf7, 0xb7, 0x49, 0xac, 0x04, 0xa4, 0x6f, 0x37, 0x33, 0xbf, 0x99, 0xdd, 0xdf, 0x6f, + 0x66, 0xe7, 0x60, 0xa8, 0x98, 0x3c, 0x8b, 0x42, 0xf6, 0x24, 0x14, 0x52, 0xb2, 0xa9, 0x90, 0xfe, + 0x5c, 0x8a, 0x44, 0x90, 0xed, 0xdc, 0x76, 0x6f, 0x87, 0x22, 0x9e, 0x0b, 0xc5, 0x0e, 0x78, 0x10, + 0x33, 0x35, 0x0f, 0x42, 0x66, 0x20, 0xee, 0x20, 0x0f, 0xc4, 0x62, 0x9a, 0xce, 0x2e, 0x78, 0x25, + 0x0b, 0x85, 0x9c, 0xa2, 0x77, 0x5f, 0x2d, 0x54, 0xc2, 0xe2, 0x83, 0x38, 0x88, 0x66, 0x5f, 0x62, + 0xa6, 0x54, 0x70, 0x8a, 0x09, 0xde, 0x21, 0xdc, 0xa4, 0x29, 0xff, 0xcc, 0x54, 0x42, 0xd9, 0x8f, + 0x94, 0xa9, 0x84, 0x0c, 0xc1, 0x56, 0x22, 0x95, 0x21, 0x73, 0xac, 0x91, 0x35, 0xee, 0x52, 0xb4, + 0x08, 0x81, 0x4e, 0x76, 0x07, 0xa7, 0xa5, 0xbd, 0xfa, 0xdb, 0x3b, 0xb7, 0x60, 0x8f, 0xa6, 0xfc, + 0x43, 0x7e, 0xb7, 0xbc, 0xc6, 0x11, 0xd8, 0xa1, 0xe0, 0xdf, 0xa2, 0x53, 0xc7, 0x1a, 0xb5, 0xc7, + 0xbd, 0xc9, 0x23, 0xbf, 0x20, 0xb8, 0x04, 0xee, 0x1f, 0x6b, 0xec, 0x5b, 0x9e, 0xc8, 0x05, 0xc5, + 0x44, 0x32, 0x06, 0x5b, 0x85, 0x32, 0x9a, 0x27, 0xfa, 0xc0, 0xde, 0xa4, 0x5f, 0x96, 0xf8, 0xa4, + 0xfd, 0x14, 0xe3, 0xe4, 0x29, 0x74, 0x0b, 0x71, 0x9c, 0xb6, 0x06, 0x13, 0x1f, 0x75, 0xf0, 0xcb, + 0xb3, 0x4a, 0x90, 0xfb, 0x12, 0x7a, 0x95, 0x23, 0x49, 0x1f, 0xda, 0xdf, 0xd9, 0x02, 0xe9, 0x66, + 0x9f, 0x64, 0x00, 0x5b, 0x67, 0xc1, 0x2c, 0xcd, 0xc9, 0x1a, 0xe3, 0x55, 0xeb, 0x85, 0xe5, 0xfd, + 0x6c, 0x41, 0x9f, 0xa6, 0xfc, 0x44, 0x8b, 0x9e, 0xd3, 0x7d, 0xd3, 0xa0, 0xfb, 0xa0, 0x46, 0xb7, + 0x86, 0xfd, 0xdf, 0x5c, 0xc9, 0x43, 0xb0, 0xcd, 0x84, 0x38, 0x1d, 0x0d, 0xdf, 0x29, 0xe0, 0x78, + 0x2f, 0x0c, 0x6f, 0x22, 0xca, 0x2f, 0x23, 0x0a, 0xd5, 0x33, 0x77, 0x39, 0x51, 0x6a, 0xd8, 0x2b, + 0x2b, 0x4a, 0x06, 0x34, 0x2f, 0xc9, 0xd9, 0x6a, 0x00, 0x91, 0x00, 0x86, 0x37, 0x51, 0xef, 0xb7, + 0x05, 0xb7, 0xb2, 0x31, 0x09, 0xa2, 0xd9, 0x89, 0x79, 0x9b, 0xb9, 0x84, 0xc7, 0x0d, 0x09, 0x1f, + 0xd7, 0xe7, 0xea, 0x42, 0xc2, 0x86, 0x3a, 0x3e, 0x87, 0x5e, 0x5c, 0xd6, 0x44, 0x25, 0xf7, 0x7c, + 0xb3, 0x3c, 0xfc, 0xea, 0x71, 0x55, 0xdc, 0x26, 0xd4, 0x77, 0x61, 0xa7, 0xd8, 0x3e, 0x6a, 0x2e, + 0xb8, 0x62, 0xde, 0x3b, 0x18, 0xd4, 0x57, 0x84, 0xf1, 0xd7, 0x9b, 0x6c, 0x5d, 0xa2, 0xc9, 0xde, + 0x21, 0xec, 0x56, 0x5e, 0x1f, 0x96, 0x29, 0x3b, 0x6f, 0xad, 0xed, 0x3c, 0x66, 0xe7, 0x63, 0x5a, + 0x66, 0xe3, 0x38, 0x58, 0x6b, 0xc7, 0xc1, 0x73, 0x60, 0xd8, 0xec, 0x10, 0xf2, 0x9b, 0x82, 0x6d, + 0x64, 0xff, 0x97, 0x45, 0x4b, 0x1c, 0xb8, 0x96, 0x44, 0x31, 0x13, 0x69, 0xa2, 0xdb, 0x72, 0x83, + 0xe6, 0x66, 0x26, 0x6e, 0xa0, 0x16, 0x3c, 0xd4, 0x93, 0xbc, 0x4d, 0x8d, 0x31, 0x39, 0x6f, 0xc1, + 0x75, 0xec, 0x6e, 0xca, 0x39, 0x93, 0xe4, 0x35, 0x74, 0x32, 0x99, 0x89, 0x53, 0x1b, 0xa1, 0xca, + 0xde, 0x77, 0xf7, 0x97, 0x44, 0x90, 0xf6, 0x7b, 0xe8, 0x16, 0x0a, 0x93, 0xbb, 0x6b, 0x77, 0xb9, + 0x7b, 0x6f, 0x55, 0x18, 0x6b, 0x1d, 0x81, 0x6d, 0x94, 0x26, 0xee, 0xea, 0x2d, 0xe9, 0xde, 0x59, + 0x1a, 0x2b, 0x4b, 0x18, 0xb9, 0x1b, 0x25, 0x6a, 0x3b, 0xa5, 0x51, 0xa2, 0xd1, 0xc8, 0x8f, 0xd0, + 0xab, 0x34, 0x87, 0xdc, 0xff, 0xcb, 0xc3, 0x72, 0x47, 0xab, 0x01, 0xa6, 0xe2, 0x57, 0x5b, 0xff, + 0x4f, 0x9f, 0xfd, 0x09, 0x00, 0x00, 0xff, 0xff, 0xef, 0xf0, 0x32, 0xb0, 0xd3, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -540,6 +635,7 @@ type ScriptRunnerClient interface { Namespace(ctx context.Context, in *RunNamespaceRequest, opts ...grpc.CallOption) (*RunNamespaceResponse, error) Module(ctx context.Context, in *RunModuleRequest, opts ...grpc.CallOption) (*RunModuleResponse, error) Record(ctx context.Context, in *RunRecordRequest, opts ...grpc.CallOption) (*RunRecordResponse, error) + MailMessage(ctx context.Context, in *RunMailMessageRequest, opts ...grpc.CallOption) (*RunMailMessageResponse, error) } type scriptRunnerClient struct { @@ -586,12 +682,22 @@ func (c *scriptRunnerClient) Record(ctx context.Context, in *RunRecordRequest, o return out, nil } +func (c *scriptRunnerClient) MailMessage(ctx context.Context, in *RunMailMessageRequest, opts ...grpc.CallOption) (*RunMailMessageResponse, error) { + out := new(RunMailMessageResponse) + err := c.cc.Invoke(ctx, "/corredor.ScriptRunner/MailMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ScriptRunnerServer is the server API for ScriptRunner service. type ScriptRunnerServer interface { Test(context.Context, *RunTestRequest) (*RunTestResponse, error) Namespace(context.Context, *RunNamespaceRequest) (*RunNamespaceResponse, error) Module(context.Context, *RunModuleRequest) (*RunModuleResponse, error) Record(context.Context, *RunRecordRequest) (*RunRecordResponse, error) + MailMessage(context.Context, *RunMailMessageRequest) (*RunMailMessageResponse, error) } // UnimplementedScriptRunnerServer can be embedded to have forward compatible implementations. @@ -610,6 +716,9 @@ func (*UnimplementedScriptRunnerServer) Module(ctx context.Context, req *RunModu func (*UnimplementedScriptRunnerServer) Record(ctx context.Context, req *RunRecordRequest) (*RunRecordResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Record not implemented") } +func (*UnimplementedScriptRunnerServer) MailMessage(ctx context.Context, req *RunMailMessageRequest) (*RunMailMessageResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method MailMessage not implemented") +} func RegisterScriptRunnerServer(s *grpc.Server, srv ScriptRunnerServer) { s.RegisterService(&_ScriptRunner_serviceDesc, srv) @@ -687,6 +796,24 @@ func _ScriptRunner_Record_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } +func _ScriptRunner_MailMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RunMailMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScriptRunnerServer).MailMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/corredor.ScriptRunner/MailMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScriptRunnerServer).MailMessage(ctx, req.(*RunMailMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _ScriptRunner_serviceDesc = grpc.ServiceDesc{ ServiceName: "corredor.ScriptRunner", HandlerType: (*ScriptRunnerServer)(nil), @@ -707,6 +834,10 @@ var _ScriptRunner_serviceDesc = grpc.ServiceDesc{ MethodName: "Record", Handler: _ScriptRunner_Record_Handler, }, + { + MethodName: "MailMessage", + Handler: _ScriptRunner_MailMessage_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "service-corredor.proto", diff --git a/pkg/automation/mail/condition.go b/pkg/automation/mail/condition.go new file mode 100644 index 000000000..d6472975e --- /dev/null +++ b/pkg/automation/mail/condition.go @@ -0,0 +1,196 @@ +package mail + +import ( + "encoding/json" + "errors" + "net/mail" + "net/textproto" + "regexp" + "strings" + + "github.com/cortezaproject/corteza-server/pkg/automation" + "github.com/cortezaproject/corteza-server/system/types" +) + +// Trigger condition: +// Matcher for mail headers + +type ( + Condition struct { + MatchAll bool `json:"matchAll"` + Headers []HeaderMatcher `json:"headers"` + } + + HeaderMatcher struct { + Name HMName `json:"name"` + Op HMOp `json:"op"` + Match string `json:"match"` + + // Compiled regexp + re *regexp.Regexp + } + + HMName string + HMOp string + + userExistanceVerifier func(string) bool +) + +const ( + HeaderMatchNameFrom HMName = "from" + HeaderMatchNameTo = "to" + HeaderMatchNameCC = "cc" + HeaderMatchNameBCC = "bcc" + HeaderMatchNameReplyTo = "reply-to" + HeaderMatchNameSubject = "subject" + + // Keeping -ci suffix in case we get + // feature request to separete ci & cs operators + HMOpEqualCi HMOp = "equal-ci" + HMOpSuffixCi = "suffix-ci" + HMOpPrefixCi = "prefix-ci" + HMOpRegex = "regex" + HMOpUser = "user" +) + +var ( + ErrUnknownHeaderMatcherName = errors.New("unknown header matcher field") + ErrUnknownHeaderMatcherOperator = errors.New("unknown header matcher operator") + ErrInvalidHeaderMatcherValue = errors.New("invalid header matcher value") +) + +func (c *Condition) Prepare() (err error) { + for i := range c.Headers { + err = c.Headers[i].prepare() + if err != nil { + return + } + } + + return +} + +// IsValid verifies if header matcher is valid +func (m *HeaderMatcher) prepare() (err error) { + switch m.Name { + case HeaderMatchNameFrom, + HeaderMatchNameTo, + HeaderMatchNameCC, + HeaderMatchNameBCC, + HeaderMatchNameReplyTo, + HeaderMatchNameSubject: + // ok fields + default: + return ErrUnknownHeaderMatcherName + } + + switch m.Op { + case HMOpRegex: + // Try to compile given regex + m.re, err = regexp.Compile(m.Match) + if err != nil { + return ErrInvalidHeaderMatcherValue + } + case HMOpUser: + // When matching against existing user, + // there should be no value set + if m.Match != "" { + return ErrInvalidHeaderMatcherValue + } + case HMOpEqualCi, HMOpSuffixCi, HMOpPrefixCi: + // no special validation here + m.Match = strings.ToLower(m.Match) + default: + return ErrUnknownHeaderMatcherOperator + } + + return +} + +func (n HMName) match(name string) bool { + return string(n) == strings.ToLower(name) +} + +// IsMatch checks if header matcher matches against given headers +func (m *HeaderMatcher) isMatch(header mail.Header, exists userExistanceVerifier, matchAll bool) (match bool) { + var lcHeader string + + for name, vv := range header { + if !m.Name.match(name) { + continue + } + + for _, v := range vv { + lcHeader = strings.ToLower(v) + + switch m.Op { + case HMOpEqualCi: + match = m.Match == lcHeader + case HMOpSuffixCi: + match = strings.HasSuffix(lcHeader, m.Match) + case HMOpPrefixCi: + match = strings.HasPrefix(lcHeader, m.Match) + case HMOpRegex: + match = m.re.MatchString(v) + case HMOpUser: + match = exists(v) + default: + return false + } + + if !match && matchAll { + // fail in first non-match + return false + } else if match && !matchAll { + // match in first + return true + } + } + } + + return match +} + +func (c *Condition) CheckHeader(header mail.Header, uev userExistanceVerifier) (match bool) { + _ = c.Prepare() + + // Pre-process & simplify header values: parse all addresses, + // extract emails and toss away names, we do not need them + for name := range header { + switch textproto.CanonicalMIMEHeaderKey(name) { + case "From", "To", "Cc", "Bcc", "Reply-To": + for i, v := range header[name] { + addr, _ := mail.ParseAddress(v) + header[name][i] = strings.Trim(addr.Address, "><") + } + } + } + + for _, h := range c.Headers { + match = h.isMatch(header, uev, c.MatchAll) + + if !match && c.MatchAll { + // fail in first non-match + return false + } else if match && !c.MatchAll { + // match in first + return true + } + } + + return match +} + +func MakeChecker(headers types.MailMessageHeader, uev userExistanceVerifier) automation.TriggerConditionChecker { + return func(c string) bool { + var ( + tc = Condition{} + ) + + if err := json.Unmarshal([]byte(c), &tc); err == nil { + return tc.CheckHeader(headers.Raw, uev) + } + + return false + } +} diff --git a/pkg/automation/mail/condition_test.go b/pkg/automation/mail/condition_test.go new file mode 100644 index 000000000..010124cd4 --- /dev/null +++ b/pkg/automation/mail/condition_test.go @@ -0,0 +1,144 @@ +package mail + +import ( + "encoding/json" + "testing" + + "github.com/cortezaproject/corteza-server/system/types" +) + +func Test_makeMailHeaderChecker(t *testing.T) { + tests := []struct { + name string + mh types.MailMessageHeader + tc Condition + expecting bool + }{ + { + name: "empty should not match", + mh: types.MailMessageHeader{}, + tc: Condition{}, + }, + { + name: "simple check", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Op: HMOpEqualCi, Match: "SIMPLE"}}}, + expecting: true, + }, + { + name: "simple check - no match", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Op: HMOpEqualCi, Match: "complex"}}}, + expecting: false, + }, + { + name: "simple check - no match", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameFrom, Op: HMOpEqualCi, Match: "SIMPLE"}}}, + expecting: false, + }, + { + name: "simple check - name-case", + mh: types.MailMessageHeader{Raw: map[string][]string{"SUBJECT": []string{"SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Op: HMOpEqualCi, Match: "SIMPLE"}}}, + expecting: true, + }, + { + name: "check address (brackets)", + mh: types.MailMessageHeader{Raw: map[string][]string{"From": []string{""}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameFrom, Op: HMOpEqualCi, Match: "some@mail.tld"}}}, + expecting: true, + }, + { + name: "check address (bare)", + mh: types.MailMessageHeader{Raw: map[string][]string{"From": []string{"some@mail.tld"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameFrom, Op: HMOpEqualCi, Match: "some@mail.tld"}}}, + expecting: true, + }, + { + name: "check address (full, quoted)", + mh: types.MailMessageHeader{Raw: map[string][]string{"From": []string{`"John Doe" `}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameFrom, Op: HMOpEqualCi, Match: "some@mail.tld"}}}, + expecting: true, + }, + { + name: "check address (full)", + mh: types.MailMessageHeader{Raw: map[string][]string{"From": []string{`John Doe `}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameFrom, Op: HMOpEqualCi, Match: "some@mail.tld"}}}, + expecting: true, + }, + { + name: "two matchers, one matches", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, + tc: Condition{ + Headers: []HeaderMatcher{ + {Name: HeaderMatchNameSubject, Op: HMOpEqualCi, Match: "SIMPLE"}, + {Name: HeaderMatchNameSubject, Op: HMOpEqualCi, Match: "complex"}, + }, + }, + expecting: true, + }, + { + name: "two matchers, one matches, match-all=true", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, + tc: Condition{ + MatchAll: true, + Headers: []HeaderMatcher{ + {Name: HeaderMatchNameSubject, Op: HMOpEqualCi, Match: "SIMPLE"}, + {Name: HeaderMatchNameSubject, Op: HMOpEqualCi, Match: "complex"}, + }, + }, + expecting: false, + }, + { + name: "match by prefix", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"abcd"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Op: HMOpPrefixCi, Match: "ab"}}}, + expecting: true, + }, + { + name: "match by prefix", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"abcd"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Op: HMOpPrefixCi, Match: "cd"}}}, + expecting: false, + }, + { + name: "match by suffix", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"abcd"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Op: HMOpSuffixCi, Match: "cd"}}}, + expecting: true, + }, + { + name: "match by suffix", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"abcd"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Op: HMOpSuffixCi, Match: "ab"}}}, + expecting: false, + }, + { + name: "regex check", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "^S.+$", Op: HMOpRegex}}}, + expecting: true, + }, + { + name: "case-insensitive check", + mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "simple", Op: HMOpEqualCi}}}, + expecting: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.tc.Prepare(); err != nil { + t.Errorf("unable to prepare header matcher: %v", err) + } + + checker := MakeChecker(tt.mh, nil) + + j, _ := json.Marshal(tt.tc) + if checker(string(j)) != tt.expecting { + t.Errorf("did not match (expecting: %v)", tt.expecting) + } + }) + } +} diff --git a/system/commands/sink.go b/system/commands/sink.go new file mode 100644 index 000000000..6440922ac --- /dev/null +++ b/system/commands/sink.go @@ -0,0 +1,89 @@ +package commands + +import ( + "context" + "net/url" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/pkg/cli" +) + +// Will perform OpenID connect auto-configuration +func Sink(ctx context.Context, c *cli.Config) *cobra.Command { + var ( + expires string + origin string + contentType string + method string + ) + + cmd := &cobra.Command{ + Use: "sink", + Short: "Sink", + } + + signatureCmd := &cobra.Command{ + Use: "signature", + Short: "Creates signature for sink HTTP endpoint", + RunE: func(cmd *cobra.Command, args []string) error { + c.InitServices(ctx, c) + + method = strings.ToUpper(method) + + if expires != "" { + // validate expiration date if set + if _, err := time.Parse("2006-01-02", expires); err != nil { + return err + } + } + + v := url.Values{} + v.Set("sign", auth.DefaultSigner.Sign(0, method, "/sink", contentType, origin, expires)) + v.Set("expires", expires) + v.Set("content-type", contentType) + v.Set("origin", origin) + v.Set("method", method) + + // @todo add host & schema + cmd.Println((&url.URL{ + Path: "/sink", + RawQuery: v.Encode()}).String()) + + return nil + }, + } + + signatureCmd.Flags().StringVar( + &origin, + "origin", + "", + "Origin of the request (arbitrary string, optional)") + + signatureCmd.Flags().StringVar( + &contentType, + "content-type", + "", + "Content type (optional)") + + signatureCmd.Flags().StringVar( + &expires, + "expires", + "", + "Date of expiration (YYYY-MM-DD, optional)") + + signatureCmd.Flags().StringVar( + &method, + "method", + "GET", + "HTTP method that will be used") + + cmd.AddCommand( + signatureCmd, + ) + + return cmd +} diff --git a/system/db/mysql/static.go b/system/db/mysql/static.go index 39952afa4..ee749b8e4 100644 --- a/system/db/mysql/static.go +++ b/system/db/mysql/static.go @@ -3,4 +3,4 @@ // Package contains static assets. package mysql -var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x00 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose:*', 'access', 2),\n (1, 'messaging:*', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace:*', 'page.create', 2),\n (2, 'compose:namespace:*', 'read', 2),\n (2, 'compose:namespace:*', 'update', 2),\n (2, 'compose:namespace:*', 'delete', 2),\n (2, 'compose:namespace:*', 'module.create', 2),\n (2, 'compose:namespace:*', 'chart.create', 2),\n (2, 'compose:namespace:*', 'trigger.create', 2),\n (2, 'compose:chart:*', 'read', 2),\n (2, 'compose:chart:*', 'update', 2),\n (2, 'compose:chart:*', 'delete', 2),\n (2, 'compose:trigger:*', 'read', 2),\n (2, 'compose:trigger:*', 'update', 2),\n (2, 'compose:trigger:*', 'delete', 2),\n (2, 'compose:page:*', 'read', 2),\n (2, 'compose:page:*', 'update', 2),\n (2, 'compose:page:*', 'delete', 2),\n (2, 'system', 'access', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation:*', 'access', 2),\n (2, 'system:role:*', 'read', 2),\n (2, 'system:role:*', 'update', 2),\n (2, 'system:role:*', 'delete', 2),\n (2, 'system:role:*', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 2),\n (2, 'messaging:channel:*', 'message.react', 2),\n (2, 'compose:module:*', 'read', 2),\n (2, 'compose:module:*', 'update', 2),\n (2, 'compose:module:*', 'delete', 2),\n (2, 'compose:module:*', 'record.create', 2),\n (2, 'compose:module:*', 'record.read', 2),\n (2, 'compose:module:*', 'record.update', 2),\n (2, 'compose:module:*', 'record.delete', 2);\nPK\x07\x08z\",\xe8t\x0b\x00\x00t\x0b\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS `settings`;\n\nCREATE TABLE IF NOT EXISTS `sys_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08`\xcb\x1b\x81t\x02\x00\x00t\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP `password`;\nALTER TABLE `sys_user` DROP `satosa_id`;\nALTER TABLE `sys_credentials` ADD `last_used_at` DATETIME NULL;\nPK\x07\x088\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190405090000.internal-auth.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` ADD `email_confirmed` BOOLEAN NOT NULL DEFAULT FALSE;\nPK\x07\x08\x8fQs\x8cM\x00\x00\x00M\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190506090000.compose-app.up.sqlUT\x05\x00\x01\x80Cm8UPDATE `sys_application`\n SET `name` = 'Crust Compose',\n `unify` = '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/compose/\", \"listed\": true}'\n WHERE id = 2;\nPK\x07\x08\x10\xe9%]\xd0\x00\x00\x00\xd0\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190506090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS sys_permission_rules;\nDROP TABLE IF EXISTS messaging_permission_rules;\nDROP TABLE IF EXISTS compose_permission_rules;\n\nCREATE TABLE IF NOT EXISTS sys_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\n\nCREATE TABLE IF NOT EXISTS messaging_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\n\nCREATE TABLE IF NOT EXISTS compose_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\n\nREPLACE sys_permission_rules\n (rel_role, resource, operation, access)\n SELECT rel_role, resource, operation, `value` - 1 FROM sys_rules WHERE resource LIKE 'system%';\n\nREPLACE compose_permission_rules\n (rel_role, resource, operation, access)\n SELECT rel_role, resource, operation, `value` - 1 FROM sys_rules WHERE resource LIKE 'compose%';\n\nREPLACE messaging_permission_rules\n (rel_role, resource, operation, access)\n SELECT rel_role, resource, operation, `value` - 1 FROM sys_rules WHERE resource LIKE 'messaging%';\n\nDROP TABLE sys_rules;\nPK\x07\x08\x84\xf6H\xe4\xf1\x05\x00\x00\xf1\x05\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x00 \x0020190826085348.migrate-gplus-google.up.sqlUT\x05\x00\x01\x80Cm8/* migrates existing credentials */\nUPDATE sys_credentials SET kind = 'google' WHERE kind = 'gplus';\n\n/* migrates existing settings. */\nUPDATE sys_settings SET name = REPLACE(name, '.gplus.', '.google.') WHERE name LIKE 'auth.external.providers.gplus.%';\nPK\x07\x08<\xac\xedE\xff\x00\x00\x00\xff\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(z\",\xe8t\x0b\x00\x00t\x0b\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(`\xcb\x1b\x81t\x02\x00\x00t\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(8\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x93*\x00\x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x8fQs\x8cM\x00\x00\x00M\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81~+\x00\x0020190405090000.internal-auth.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x10\xe9%]\xd0\x00\x00\x00\xd0\x00\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81%,\x00\x0020190506090000.compose-app.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x84\xf6H\xe4\xf1\x05\x00\x00\xf1\x05\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81M-\x00\x0020190506090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(<\xac\xedE\xff\x00\x00\x00\xff\x00\x00\x00*\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xb4\x81\x963\x00\x0020190826085348.migrate-gplus-google.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xf64\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\xb36\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x12\x00\x12\x00@\x06\x00\x00\x1e7\x00\x00\x00\x00" +var Asset = "PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00 \x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8-- all known organisations (crust instances) and our relation towards them\nCREATE TABLE organisations (\n id BIGINT UNSIGNED NOT NULL,\n fqn TEXT NOT NULL, -- fully qualified name of the organisation\n name TEXT NOT NULL, -- display name of the organisation\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- organisation soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE settings (\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value TEXT COMMENT 'Setting value',\n\n PRIMARY KEY (name)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE users (\n id BIGINT UNSIGNED NOT NULL,\n email TEXT NOT NULL,\n username TEXT NOT NULL,\n password TEXT NOT NULL,\n name TEXT NOT NULL,\n handle TEXT NOT NULL,\n meta JSON NOT NULL,\n satosa_id CHAR(36) NULL,\n\n rel_organisation BIGINT UNSIGNED NOT NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n suspended_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE UNIQUE INDEX uid_satosa ON users (satosa_id);\n\n-- Keeps all known teams\nCREATE TABLE teams (\n id BIGINT UNSIGNED NOT NULL,\n name TEXT NOT NULL, -- display name of the team\n handle TEXT NOT NULL, -- team handle string\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n archived_at DATETIME NULL,\n deleted_at DATETIME NULL, -- team soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n-- Keeps team memberships\nCREATE TABLE team_members (\n rel_team BIGINT UNSIGNED NOT NULL REFERENCES organisation(id),\n rel_user BIGINT UNSIGNED NOT NULL,\n\n PRIMARY KEY (rel_team, rel_user)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xedzU\x8am \x00\x00m \x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00.\x00 \x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE teams RENAME TO sys_team;\nALTER TABLE organisations RENAME TO sys_organisation;\nALTER TABLE team_members RENAME TO sys_team_member;\nALTER TABLE users RENAME TO sys_user;PK\x07\x08\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8# add field to manage user type (bot support)\nALTER TABLE `sys_user` ADD `kind` VARCHAR(8) NOT NULL DEFAULT '' AFTER `handle`;\n\n# add field to manage \"ownership\" (get all bots created by user)\nALTER TABLE `sys_user` ADD `rel_user_id` BIGINT UNSIGNED NOT NULL AFTER `rel_organisation`, ADD INDEX (`rel_user_id`);\nPK\x07\x089\xa0\xdat8\x01\x00\x008\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00 \x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP INDEX `uid_satosa`, ADD INDEX `uid_satosa` (`satosa_id`) USING BTREE;PK\x07\x08\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8-- Keeps all known users, home and external organisation\n-- changes are stored in audit log\nCREATE TABLE sys_credentials (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n label TEXT NOT NULL COMMENT 'something we can differentiate credentials by',\n kind VARCHAR(128) NOT NULL COMMENT 'hash, facebook, gplus, github, linkedin ...',\n credentials TEXT NOT NULL COMMENT 'crypted/hashed passwords, secrets, social profile ID',\n meta JSON NOT NULL,\n expires_at DATETIME NULL,\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE INDEX idx_owner ON sys_credentials (rel_owner);\nPK\x07\x08f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` MODIFY `password` TEXT NULL;\nPK\x07\x080V\x13\x0f4\x00\x00\x004\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1b\x00 \x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE `sys_rules` (\n `rel_team` BIGINT UNSIGNED NOT NULL,\n `resource` VARCHAR(128) NOT NULL,\n `operation` VARCHAR(128) NOT NULL,\n `value` TINYINT(1) NOT NULL,\n\n PRIMARY KEY (`rel_team`, `resource`, `operation`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00)\x00 \x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE sys_team RENAME TO sys_role;\nALTER TABLE sys_team_member RENAME TO sys_role_member;\n\nALTER TABLE `sys_role_member` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nALTER TABLE `sys_rules` CHANGE COLUMN `rel_team` `rel_role` BIGINT UNSIGNED NOT NULL;\nPK\x07\x08s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00,\x00 \x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8REPLACE INTO `sys_role` (`id`, `name`, `handle`) VALUES\n (1, 'Everyone', 'everyone'),\n (2, 'Administrators', 'admins');\n\n-- Value: Allow (2), Deny (1), Inherit(0),\nREPLACE INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n-- Everyone\n (1, 'compose:*', 'access', 2),\n (1, 'messaging:*', 'access', 2),\n-- Admins\n (2, 'compose', 'namespace.create', 2),\n (2, 'compose', 'access', 2),\n (2, 'compose', 'grant', 2),\n (2, 'compose:namespace:*', 'page.create', 2),\n (2, 'compose:namespace:*', 'read', 2),\n (2, 'compose:namespace:*', 'update', 2),\n (2, 'compose:namespace:*', 'delete', 2),\n (2, 'compose:namespace:*', 'module.create', 2),\n (2, 'compose:namespace:*', 'chart.create', 2),\n (2, 'compose:namespace:*', 'trigger.create', 2),\n (2, 'compose:chart:*', 'read', 2),\n (2, 'compose:chart:*', 'update', 2),\n (2, 'compose:chart:*', 'delete', 2),\n (2, 'compose:trigger:*', 'read', 2),\n (2, 'compose:trigger:*', 'update', 2),\n (2, 'compose:trigger:*', 'delete', 2),\n (2, 'compose:page:*', 'read', 2),\n (2, 'compose:page:*', 'update', 2),\n (2, 'compose:page:*', 'delete', 2),\n (2, 'system', 'access', 2),\n (2, 'system', 'grant', 2),\n (2, 'system', 'organisation.create', 2),\n (2, 'system', 'role.create', 2),\n (2, 'system:organisation:*', 'access', 2),\n (2, 'system:role:*', 'read', 2),\n (2, 'system:role:*', 'update', 2),\n (2, 'system:role:*', 'delete', 2),\n (2, 'system:role:*', 'members.manage', 2),\n (2, 'messaging', 'access', 2),\n (2, 'messaging', 'grant', 2),\n (2, 'messaging', 'channel.public.create', 2),\n (2, 'messaging', 'channel.private.create', 2),\n (2, 'messaging', 'channel.group.create', 2),\n (2, 'messaging:channel:*', 'update', 2),\n (2, 'messaging:channel:*', 'leave', 2),\n (2, 'messaging:channel:*', 'read', 2),\n (2, 'messaging:channel:*', 'join', 2),\n (2, 'messaging:channel:*', 'delete', 2),\n (2, 'messaging:channel:*', 'undelete', 2),\n (2, 'messaging:channel:*', 'archive', 2),\n (2, 'messaging:channel:*', 'unarchive', 2),\n (2, 'messaging:channel:*', 'members.manage', 2),\n (2, 'messaging:channel:*', 'webhooks.manage', 2),\n (2, 'messaging:channel:*', 'attachments.manage', 2),\n (2, 'messaging:channel:*', 'message.attach', 2),\n (2, 'messaging:channel:*', 'message.update.all', 2),\n (2, 'messaging:channel:*', 'message.update.own', 2),\n (2, 'messaging:channel:*', 'message.delete.all', 2),\n (2, 'messaging:channel:*', 'message.delete.own', 2),\n (2, 'messaging:channel:*', 'message.embed', 2),\n (2, 'messaging:channel:*', 'message.send', 2),\n (2, 'messaging:channel:*', 'message.reply', 2),\n (2, 'messaging:channel:*', 'message.react', 2),\n (2, 'compose:module:*', 'read', 2),\n (2, 'compose:module:*', 'update', 2),\n (2, 'compose:module:*', 'delete', 2),\n (2, 'compose:module:*', 'record.create', 2),\n (2, 'compose:module:*', 'record.read', 2),\n (2, 'compose:module:*', 'record.update', 2),\n (2, 'compose:module:*', 'record.delete', 2);\nPK\x07\x08z\",\xe8t\x0b\x00\x00t\x0b\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\"\x00 \x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE sys_application (\n id BIGINT UNSIGNED NOT NULL,\n rel_owner BIGINT UNSIGNED NOT NULL REFERENCES sys_users(id),\n name TEXT NOT NULL COMMENT 'something we can differentiate application by',\n enabled BOOL NOT NULL,\n\n unify JSON NULL COMMENT 'unify specific settings',\n\n created_at DATETIME NOT NULL DEFAULT NOW(),\n updated_at DATETIME NULL,\n deleted_at DATETIME NULL, -- user soft delete\n\n PRIMARY KEY (id)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\n\nREPLACE INTO `sys_application` (`id`, `name`, `enabled`, `rel_owner`, `unify`) VALUES\n( 1, 'Crust Messaging', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/messaging/\", \"listed\": true}'\n),\n( 2, 'Crust CRM', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/crm/\", \"listed\": true}'\n),\n( 3, 'Crust Admin Area', true, 0,\n '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/admin/\", \"listed\": true}'\n),\n( 4, 'Corteza Jitsi Bridge', true, 0,\n '{\"logo\": \"/applications/jitsi.png\", \"icon\": \"/applications/jitsi_icon.png\", \"url\": \"/bridge/jitsi/\", \"listed\": true}'\n),\n( 5, 'Google Maps', true, 0,\n '{\"logo\": \"/applications/google_maps.png\", \"icon\": \"/applications/google_maps_icon.png\", \"url\": \"/bridge/google-maps/\", \"listed\": true}'\n);\n\n-- Allow admin access to applications\nINSERT INTO `sys_rules` (`rel_role`, `resource`, `operation`, `value`) VALUES\n (1, 'system:application:*', 'read', 2),\n (2, 'system', 'application.create', 2),\n (2, 'system:application:*', 'read', 2),\n (2, 'system:application:*', 'update', 2),\n (2, 'system:application:*', 'delete', 2)\n;\nPK\x07\x08\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00 \x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS `settings`;\n\nCREATE TABLE IF NOT EXISTS `sys_settings` (\n rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',\n name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',\n value JSON COMMENT 'Setting value',\n\n updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',\n updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',\n\n PRIMARY KEY (name, rel_owner)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08`\xcb\x1b\x81t\x02\x00\x00t\x02\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` DROP `password`;\nALTER TABLE `sys_user` DROP `satosa_id`;\nALTER TABLE `sys_credentials` ADD `last_used_at` DATETIME NULL;\nPK\x07\x088\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00 \x0020190405090000.internal-auth.up.sqlUT\x05\x00\x01\x80Cm8ALTER TABLE `sys_user` ADD `email_confirmed` BOOLEAN NOT NULL DEFAULT FALSE;\nPK\x07\x08\x8fQs\x8cM\x00\x00\x00M\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190506090000.compose-app.up.sqlUT\x05\x00\x01\x80Cm8UPDATE `sys_application`\n SET `name` = 'Crust Compose',\n `unify` = '{\"logo\": \"/applications/crust.jpg\", \"icon\": \"/applications/crust_favicon.png\", \"url\": \"/compose/\", \"listed\": true}'\n WHERE id = 2;\nPK\x07\x08\x10\xe9%]\xd0\x00\x00\x00\xd0\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00 \x0020190506090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8DROP TABLE IF EXISTS sys_permission_rules;\nDROP TABLE IF EXISTS messaging_permission_rules;\nDROP TABLE IF EXISTS compose_permission_rules;\n\nCREATE TABLE IF NOT EXISTS sys_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\n\nCREATE TABLE IF NOT EXISTS messaging_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\n\nCREATE TABLE IF NOT EXISTS compose_permission_rules (\n rel_role BIGINT UNSIGNED NOT NULL,\n resource VARCHAR(128) NOT NULL,\n operation VARCHAR(128) NOT NULL,\n access TINYINT(1) NOT NULL,\n\n PRIMARY KEY (rel_role, resource, operation)\n) ENGINE=InnoDB;\n\nREPLACE sys_permission_rules\n (rel_role, resource, operation, access)\n SELECT rel_role, resource, operation, `value` - 1 FROM sys_rules WHERE resource LIKE 'system%';\n\nREPLACE compose_permission_rules\n (rel_role, resource, operation, access)\n SELECT rel_role, resource, operation, `value` - 1 FROM sys_rules WHERE resource LIKE 'compose%';\n\nREPLACE messaging_permission_rules\n (rel_role, resource, operation, access)\n SELECT rel_role, resource, operation, `value` - 1 FROM sys_rules WHERE resource LIKE 'messaging%';\n\nDROP TABLE sys_rules;\nPK\x07\x08\x84\xf6H\xe4\xf1\x05\x00\x00\xf1\x05\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00*\x00 \x0020190826085348.migrate-gplus-google.up.sqlUT\x05\x00\x01\x80Cm8/* migrates existing credentials */\nUPDATE sys_credentials SET kind = 'google' WHERE kind = 'gplus';\n\n/* migrates existing settings. */\nUPDATE sys_settings SET name = REPLACE(name, '.gplus.', '.google.') WHERE name LIKE 'auth.external.providers.gplus.%';\nPK\x07\x08<\xac\xedE\xff\x00\x00\x00\xff\x00\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00 \x0020190902080000.automation.up.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS sys_automation_script (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `name` VARCHAR(64) NOT NULL DEFAULT 'unnamed' COMMENT 'The name of the script',\n `source` TEXT NOT NULL COMMENT 'Source code for the script',\n `source_ref` VARCHAR(200) NOT NULL COMMENT 'Where is the script located (if remote)',\n `async` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Do we run this script asynchronously?',\n `rel_runner` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who is running the script? 0 for invoker',\n `run_in_ua` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Run this script inside user-agent environment',\n `timeout` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Any explicit timeout set for this script (milliseconds)?',\n `critical` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is it critical that this script is executed successfully',\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is this script enabled?',\n\n `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `updated_at` DATETIME NULL DEFAULT NULL,\n `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `deleted_at` DATETIME NULL DEFAULT NULL,\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nCREATE TABLE IF NOT EXISTS sys_automation_trigger (\n `id` BIGINT(20) UNSIGNED NOT NULL,\n `rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is triggered',\n\n `resource` VARCHAR(128) NOT NULL COMMENT 'Resource triggering the event',\n `event` VARCHAR(128) NOT NULL COMMENT 'Event triggered',\n `event_condition`\n TEXT NOT NULL COMMENT 'Trigger condition',\n `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?',\n\n `weight` INT NOT NULL DEFAULT 0,\n\n `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `updated_at` DATETIME NULL DEFAULT NULL,\n `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,\n `deleted_at` DATETIME NULL DEFAULT NULL,\n\n CONSTRAINT `fk_sys_automation_script` FOREIGN KEY (`rel_script`) REFERENCES `sys_automation_script` (`id`),\n\n PRIMARY KEY (`id`)\n\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\nPK\x07\x08\xf1\x0d\x1b\xe7\xc9\n\x00\x00\xc9\n\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00 \x00migrations.sqlUT\x05\x00\x01\x80Cm8CREATE TABLE IF NOT EXISTS `migrations` (\n `project` varchar(16) NOT NULL COMMENT 'sam, crm, ...',\n `filename` varchar(255) NOT NULL COMMENT 'yyyymmddHHMMSS.sql',\n `statement_index` int(11) NOT NULL COMMENT 'Statement number from SQL file',\n `status` TEXT NOT NULL COMMENT 'ok or full error message',\n PRIMARY KEY (`project`,`filename`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nPK\x07\x08\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00PK\x03\x04\x14\x00\x08\x00\x00\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00 \x00new.shUT\x05\x00\x01\x80Cm8#!/bin/bash\ntouch $(date +%Y%m%d%H%M%S).up.sqlPK\x07\x08s\xd4N*.\x00\x00\x00.\x00\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xedzU\x8am \x00\x00m \x00\x00\x1a\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x0020180704080000.base.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf2\xc4\x87\xe8\xb5\x00\x00\x00\xb5\x00\x00\x00.\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbe \x00\x0020181124181811.rename_and_prefix_tables.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(9\xa0\xdat8\x01\x00\x008\x01\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xd8\n\x00\x0020181125100429.add_user_kind_and_owner.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xf9\xd3ga\x00\x00\x00a\x00\x00\x00-\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81t\x0c\x00\x0020181125153544.satosa_index_not_unique.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(f\x1f\x08\xd0\x9a\x03\x00\x00\x9a\x03\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x819\x0d\x00\x0020181208140000.credentials.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(0V\x13\x0f4\x00\x00\x004\x00\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81+\x11\x00\x0020190103203201.users-password-null.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x05\x10[\x91\x05\x01\x00\x00\x05\x01\x00\x00\x1b\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xbf\x11\x00\x0020190116102104.rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s-\x98\xd0\x13\x01\x00\x00\x13\x01\x00\x00)\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16\x13\x00\x0020190221001051.rename-team-to-role.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(z\",\xe8t\x0b\x00\x00t\x0b\x00\x00,\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x89\x14\x00\x0020190226160000.system_roles_and_rules.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xce\xaf\xb0\x07\x11\x07\x00\x00\x11\x07\x00\x00\"\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81` \x00\x0020190306205033.applications.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(`\xcb\x1b\x81t\x02\x00\x00t\x02\x00\x00\x1e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xca'\x00\x0020190326122000.settings.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(8\x92\x0fs\x91\x00\x00\x00\x91\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x93*\x00\x0020190403113201.users-cleanup.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x8fQs\x8cM\x00\x00\x00M\x00\x00\x00#\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81~+\x00\x0020190405090000.internal-auth.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x10\xe9%]\xd0\x00\x00\x00\xd0\x00\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81%,\x00\x0020190506090000.compose-app.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x84\xf6H\xe4\xf1\x05\x00\x00\xf1\x05\x00\x00!\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81M-\x00\x0020190506090000.permissions.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(<\xac\xedE\xff\x00\x00\x00\xff\x00\x00\x00*\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x963\x00\x0020190826085348.migrate-gplus-google.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\xf1\x0d\x1b\xe7\xc9\n\x00\x00\xc9\n\x00\x00 \x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xf64\x00\x0020190902080000.automation.up.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(\x0d\xa5T2x\x01\x00\x00x\x01\x00\x00\x0e\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x16@\x00\x00migrations.sqlUT\x05\x00\x01\x80Cm8PK\x01\x02\x14\x03\x14\x00\x08\x00\x00\x00\x00\x00!(s\xd4N*.\x00\x00\x00.\x00\x00\x00\x06\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xed\x81\xd3A\x00\x00new.shUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x13\x00\x13\x00\x97\x06\x00\x00>B\x00\x00\x00\x00" diff --git a/system/db/schema/mysql/20190902080000.automation.up.sql b/system/db/schema/mysql/20190902080000.automation.up.sql new file mode 100644 index 000000000..dc5c31588 --- /dev/null +++ b/system/db/schema/mysql/20190902080000.automation.up.sql @@ -0,0 +1,48 @@ +CREATE TABLE IF NOT EXISTS sys_automation_script ( + `id` BIGINT(20) UNSIGNED NOT NULL, + `rel_namespace` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'For compatibility only, not used', + `name` VARCHAR(64) NOT NULL DEFAULT 'unnamed' COMMENT 'The name of the script', + `source` TEXT NOT NULL COMMENT 'Source code for the script', + `source_ref` VARCHAR(200) NOT NULL COMMENT 'Where is the script located (if remote)', + `async` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Do we run this script asynchronously?', + `rel_runner` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who is running the script? 0 for invoker', + `run_in_ua` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Run this script inside user-agent environment', + `timeout` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Any explicit timeout set for this script (milliseconds)?', + `critical` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is it critical that this script is executed successfully', + `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is this script enabled?', + + `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `updated_at` DATETIME NULL DEFAULT NULL, + `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `deleted_at` DATETIME NULL DEFAULT NULL, + + PRIMARY KEY (`id`) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS sys_automation_trigger ( + `id` BIGINT(20) UNSIGNED NOT NULL, + `rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is triggered', + + `resource` VARCHAR(128) NOT NULL COMMENT 'Resource triggering the event', + `event` VARCHAR(128) NOT NULL COMMENT 'Event triggered', + `event_condition` + TEXT NOT NULL COMMENT 'Trigger condition', + `enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?', + + `weight` INT NOT NULL DEFAULT 0, + + `created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `updated_at` DATETIME NULL DEFAULT NULL, + `deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0, + `deleted_at` DATETIME NULL DEFAULT NULL, + + CONSTRAINT `fk_sys_automation_script` FOREIGN KEY (`rel_script`) REFERENCES `sys_automation_script` (`id`), + + PRIMARY KEY (`id`) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/system/internal/service/access_control.go b/system/internal/service/access_control.go index 8522060ac..0fdd12e23 100644 --- a/system/internal/service/access_control.go +++ b/system/internal/service/access_control.go @@ -4,6 +4,7 @@ import ( "context" "github.com/cortezaproject/corteza-server/internal/permissions" + "github.com/cortezaproject/corteza-server/pkg/automation" "github.com/cortezaproject/corteza-server/system/types" ) @@ -76,6 +77,10 @@ func (svc accessControl) CanCreateApplication(ctx context.Context) bool { return svc.can(ctx, types.SystemPermissionResource, "application.create") } +func (svc accessControl) CanCreateAutomationScript(ctx context.Context) bool { + return svc.can(ctx, types.SystemPermissionResource, "automation-script.create") +} + func (svc accessControl) CanReadRole(ctx context.Context, rl *types.Role) bool { return svc.can(ctx, rl, "read", permissions.Allowed) } @@ -120,6 +125,26 @@ func (svc accessControl) CanDeleteUser(ctx context.Context, u *types.User) bool return svc.can(ctx, u, "delete") } +func (svc accessControl) CanReadAnyAutomationScript(ctx context.Context) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendWildcard(), "read") +} + +func (svc accessControl) CanReadAutomationScript(ctx context.Context, r *automation.Script) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendID(r.ID), "read") +} + +func (svc accessControl) CanUpdateAutomationScript(ctx context.Context, r *automation.Script) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendID(r.ID), "update") +} + +func (svc accessControl) CanDeleteAutomationScript(ctx context.Context, r *automation.Script) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendID(r.ID), "delete") +} + +func (svc accessControl) CanRunAutomationTrigger(ctx context.Context, r *automation.Trigger) bool { + return svc.can(ctx, types.AutomationScriptPermissionResource.AppendID(r.ID), "run", permissions.Allowed) +} + func (svc accessControl) can(ctx context.Context, res permissionResource, op permissions.Operation, ff ...permissions.CheckAccessFunc) bool { return svc.permissions.Can(ctx, res.PermissionResource(), op, ff...) } @@ -148,6 +173,7 @@ func (svc accessControl) DefaultRules() permissions.RuleSet { organisations = types.OrganisationPermissionResource.AppendWildcard() roles = types.RolePermissionResource.AppendWildcard() users = types.UserPermissionResource.AppendWildcard() + ascripts = types.AutomationScriptPermissionResource.AppendWildcard() allowAdm = func(res permissions.Resource, op permissions.Operation) *permissions.Rule { return permissions.AllowRule(permissions.AdminRoleID, res, op) @@ -165,6 +191,7 @@ func (svc accessControl) DefaultRules() permissions.RuleSet { allowAdm(sys, "application.create"), allowAdm(sys, "user.create"), allowAdm(sys, "role.create"), + allowAdm(sys, "automation-script.create"), allowAdm(organisations, "access"), allowAdm(applications, "read"), @@ -181,6 +208,10 @@ func (svc accessControl) DefaultRules() permissions.RuleSet { allowAdm(roles, "update"), allowAdm(roles, "delete"), allowAdm(roles, "members.manage"), + + allowAdm(ascripts, "read"), + allowAdm(ascripts, "update"), + allowAdm(ascripts, "delete"), } } @@ -197,6 +228,7 @@ func (svc accessControl) Whitelist() permissions.Whitelist { "role.create", "user.create", "application.create", + "automation-script.create", ) wl.Set( @@ -228,5 +260,17 @@ func (svc accessControl) Whitelist() permissions.Whitelist { "members.manage", ) + wl.Set( + types.AutomationScriptPermissionResource, + "read", + "update", + "delete", + ) + + wl.Set( + types.AutomationTriggerPermissionResource, + "run", + ) + return wl } diff --git a/system/internal/service/automation_runner.go b/system/internal/service/automation_runner.go new file mode 100644 index 000000000..e91d24cbe --- /dev/null +++ b/system/internal/service/automation_runner.go @@ -0,0 +1,193 @@ +package service + +import ( + "context" + "time" + + "github.com/pkg/errors" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + intAuth "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/pkg/automation" + "github.com/cortezaproject/corteza-server/pkg/automation/corredor" + mailTrigger "github.com/cortezaproject/corteza-server/pkg/automation/mail" + "github.com/cortezaproject/corteza-server/pkg/sentry" + "github.com/cortezaproject/corteza-server/system/internal/repository" + "github.com/cortezaproject/corteza-server/system/proto" + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + automationRunner struct { + opt AutomationRunnerOpt + logger *zap.Logger + runner corredor.ScriptRunnerClient + userFinder automationRunnerUserFinder + scriptFinder automationScriptsFinder + jwtEncoder intAuth.TokenEncoder + } + + automationRunnerUserFinder interface { + FindByEmail(string) (*types.User, error) + } + + automationScriptsFinder interface { + Watch(ctx context.Context) + FindRunnableScripts(resource, event string, cc ...automation.TriggerConditionChecker) automation.ScriptSet + } + + AutomationRunnerOpt struct { + ApiBaseURLSystem string + ApiBaseURLMessaging string + ApiBaseURLCompose string + } +) + +func AutomationRunner(opt AutomationRunnerOpt, f automationScriptsFinder, r corredor.ScriptRunnerClient) automationRunner { + var svc = automationRunner{ + opt: opt, + + userFinder: DefaultUser, + scriptFinder: f, + runner: r, + + logger: DefaultLogger.Named("automationRunner"), + jwtEncoder: intAuth.DefaultJwtHandler, + } + + return svc +} + +func (svc automationRunner) Watch(ctx context.Context) { + svc.scriptFinder.Watch(ctx) +} + +func (svc automationRunner) OnReceiveMailMessage(ctx context.Context, mail *types.MailMessage) error { + return svc.findMailScripts(mail.Header).Walk( + svc.makeMailScriptRunner(ctx, mail), + ) +} + +// Finds all scripts that can process email +func (svc automationRunner) findMailScripts(headers types.MailMessageHeader) automation.ScriptSet { + uev := func(email string) bool { + u, err := svc.userFinder.FindByEmail(email) + return u != nil && err == nil + } + + ss, _ := svc.scriptFinder. + FindRunnableScripts("system:mail", "onReceive", mailTrigger.MakeChecker(headers, uev)). + Filter(func(script *automation.Script) (bool, error) { + // Filter out user-agent scripts && scripts w/o defined runner. + return !script.RunInUA && script.RunAsDefined(), nil + }) + + return ss +} + +func (svc automationRunner) RecordScriptTester(ctx context.Context, source string, payload interface{}) (err error) { + // Make record script runner + // @todo figure out how to convert payload to *types.MailMessage + // runner := svc.makeMailScriptRunner(ctx, payload) + // + // return runner(&automation.Script{ + // ID: 0, + // Name: "test", + // SourceRef: "test", + // Source: source, + // Async: false, + // RunAs: 0, + // RunInUA: false, + // Timeout: 0, + // Critical: true, + // Enabled: false, + // }) + return repository.ErrNotImplemented +} + +// Runs record script +// +// We set-up script-running environment: security (definer / invoker), async, critical +// and copying values from the run to the given Record +// +func (svc automationRunner) makeMailScriptRunner(ctx context.Context, mail *types.MailMessage) func(script *automation.Script) error { + // Static request params (record gets updated + var req = &corredor.RunMailMessageRequest{ + MailMessage: proto.NewMailMessage(mail), + } + + svc.logger.Debug("preparing mail script runner", zap.Any("mail", mail)) + + return func(script *automation.Script) error { + if svc.runner == nil { + return errors.New("can not run corredor script: not connected") + } + + // This could be executed in a goroutine (by *after triggers, + // so we need to rewire the sentry panic recovery + defer sentry.Recover() + + ctx, cancelFn := context.WithTimeout(ctx, time.Second*5) + defer cancelFn() + + // Add invoker's or defined credentials/jwt + req.Config = map[string]string{ + "api.jwt": svc.getJWT(ctx, script), + + // Let the script know where the API is + "api.baseURL.system": svc.opt.ApiBaseURLSystem, + "api.baseURL.compose": svc.opt.ApiBaseURLCompose, + "api.baseURL.messaging": svc.opt.ApiBaseURLMessaging, + } + + // Add script info + req.Script = corredor.FromScript(script) + + _, err := svc.runner.MailMessage(ctx, req, grpc.WaitForReady(script.Critical)) + + if err != nil { + s, ok := status.FromError(err) + if !ok { + svc.logger.Error("unexpected error type", zap.Error(err)) + return err + } + + switch s.Code() { + case codes.FailedPrecondition: + // Sent on syntax errors: + err = errors.New(s.Message()) + case codes.Aborted: + err = errors.New(s.Message()) + case codes.InvalidArgument: + err = errors.New("invalid argument") + case codes.Internal: + err = errors.New("internal corredor error") + default: + } + + svc.logger.Info("script executed with errors", zap.Error(err)) + + if !script.Critical { + // This was not a critical call and we do not care about + // errors from script running service. + return nil + } + + return err + } + + return nil + } +} + +// Creates a new JWT for +func (svc automationRunner) getJWT(ctx context.Context, script *automation.Script) string { + if script.RunAsDefined() { + return script.Credentials() + } + + return svc.jwtEncoder.Encode(intAuth.GetIdentityFromContext(ctx)) +} diff --git a/system/internal/service/automation_script.go b/system/internal/service/automation_script.go new file mode 100644 index 000000000..3e96de5d7 --- /dev/null +++ b/system/internal/service/automation_script.go @@ -0,0 +1,158 @@ +package service + +import ( + "context" + + "go.uber.org/zap" + + intAuth "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/internal/permissions" + "github.com/cortezaproject/corteza-server/pkg/automation" +) + +type ( + automationScript struct { + logger *zap.Logger + scriptManager automationScriptManager + ac automationScriptAccessController + trg automationTrigger + } + + automationScriptManager interface { + FindScriptByID(context.Context, uint64) (*automation.Script, error) + FindScripts(context.Context, automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) + CreateScript(context.Context, *automation.Script) error + UpdateScript(context.Context, *automation.Script) error + DeleteScript(context.Context, *automation.Script) error + } + + automationScriptAccessController interface { + CanGrant(context.Context) bool + + CanCreateAutomationScript(context.Context) bool + CanReadAnyAutomationScript(context.Context) bool + CanReadAutomationScript(context.Context, *automation.Script) bool + CanUpdateAutomationScript(context.Context, *automation.Script) bool + CanDeleteAutomationScript(context.Context, *automation.Script) bool + } +) + +func AutomationScript(sm automationScriptManager) automationScript { + var svc = automationScript{ + scriptManager: sm, + logger: DefaultLogger.Named("automation-script"), + ac: DefaultAccessControl, + trg: DefaultAutomationTriggerManager, + } + + return svc +} + +func (svc automationScript) FindByID(ctx context.Context, scriptID uint64) (*automation.Script, error) { + if s, err := svc.loadCombo(ctx, scriptID); err != nil { + return nil, err + } else { + return s, nil + } +} + +func (svc automationScript) Find(ctx context.Context, f automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) { + + f.AccessCheck = permissions.InitAccessCheckFilter( + "read", + intAuth.GetIdentityFromContext(ctx).Roles(), + svc.ac.CanReadAnyAutomationScript(ctx), + ) + + return svc.scriptManager.FindScripts(ctx, f) +} + +func (svc automationScript) Create(ctx context.Context, mod *automation.Script) (err error) { + if !svc.ac.CanCreateAutomationScript(ctx) { + return ErrNoCreatePermissions.withStack() + } + + if mod.RunAs > 0 { + if !svc.ac.CanGrant(ctx) { + return ErrNoGrantPermissions + } + } + + err = mod.Triggers().Walk(func(t *automation.Trigger) error { + return svc.trg.isValid(ctx, mod, t) + }) + + if err != nil { + return + } + + return svc.scriptManager.CreateScript(ctx, mod) +} + +func (svc automationScript) Update(ctx context.Context, mod *automation.Script) (err error) { + var s *automation.Script + + if s, err = svc.loadCombo(ctx, mod.ID); err != nil { + return err + } + + if !svc.ac.CanUpdateAutomationScript(ctx, s) { + return ErrNoCreatePermissions.withStack() + } + + // Users need to have grant privileges to + // set script runner + if mod.RunAs != s.RunAs { + if !svc.ac.CanGrant(ctx) { + return ErrNoGrantPermissions + } + } + + s.Name = mod.Name + s.SourceRef = mod.SourceRef + s.Source = mod.Source + s.Async = mod.Async + s.RunAs = mod.RunAs + s.Timeout = mod.Timeout + s.Critical = mod.Critical + s.Enabled = mod.Enabled + + // Never run in a UA + s.RunInUA = false + + err = mod.Triggers().Walk(func(t *automation.Trigger) error { + return svc.trg.isValid(ctx, mod, t) + }) + + if err != nil { + return + } + + s.AddTrigger(automation.STMS_UPDATE, mod.Triggers()...) + + return svc.scriptManager.UpdateScript(ctx, s) +} + +func (svc automationScript) Delete(ctx context.Context, scriptID uint64) (err error) { + if s, err := svc.loadCombo(ctx, scriptID); err != nil { + return err + } else if !svc.ac.CanDeleteAutomationScript(ctx, s) { + return ErrNoCreatePermissions.withStack() + } else { + return svc.scriptManager.DeleteScript(ctx, s) + } +} + +func (svc automationScript) loadCombo(ctx context.Context, scriptID uint64) (s *automation.Script, err error) { + + if scriptID > 0 { + if s, err = svc.scriptManager.FindScriptByID(ctx, scriptID); err != nil { + return + } else if !svc.ac.CanReadAutomationScript(ctx, s) { + err = ErrNoCreatePermissions.withStack() + return + } + } + + return +} diff --git a/system/internal/service/automation_trigger.go b/system/internal/service/automation_trigger.go new file mode 100644 index 000000000..5c94fb601 --- /dev/null +++ b/system/internal/service/automation_trigger.go @@ -0,0 +1,120 @@ +package service + +import ( + "context" + + "go.uber.org/zap" + + "github.com/cortezaproject/corteza-server/pkg/automation" +) + +type ( + // Handles automation triggers storing and loading + automationTrigger struct { + logger *zap.Logger + triggerManager automationTriggerManager + + ac automationTriggerAccessController + } + + automationTriggerManager interface { + FindTriggerByID(context.Context, uint64) (*automation.Trigger, error) + FindTriggers(context.Context, automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error) + CreateTrigger(context.Context, *automation.Script, *automation.Trigger) error + UpdateTrigger(context.Context, *automation.Script, *automation.Trigger) error + DeleteTrigger(context.Context, *automation.Trigger) error + } + + automationTriggerAccessController interface { + CanCreateAutomationScript(context.Context) bool + CanUpdateAutomationScript(context.Context, *automation.Script) bool + } +) + +func AutomationTrigger(tm automationTriggerManager) automationTrigger { + var svc = automationTrigger{ + triggerManager: tm, + logger: DefaultLogger.Named("automation-trigger"), + + ac: DefaultAccessControl, + } + + return svc +} + +func (svc automationTrigger) FindByID(ctx context.Context, triggerID uint64) (*automation.Trigger, error) { + // @todo security check - can user read this trigger? + return svc.triggerManager.FindTriggerByID(ctx, triggerID) +} + +func (svc automationTrigger) Find(ctx context.Context, f automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error) { + // @todo security check - can user read these triggers? + return svc.triggerManager.FindTriggers(ctx, f) +} + +func (svc automationTrigger) Create(ctx context.Context, s *automation.Script, t *automation.Trigger) (err error) { + if err = svc.isValid(ctx, s, t); err != nil { + return + } + + if !svc.ac.CanUpdateAutomationScript(ctx, s) { + return ErrNoTriggerManagementPermissions + } + + return svc.triggerManager.CreateTrigger(ctx, s, t) +} + +func (svc automationTrigger) Update(ctx context.Context, s *automation.Script, t *automation.Trigger) (err error) { + if err = svc.isValid(ctx, s, t); err != nil { + return + } + + if !svc.ac.CanUpdateAutomationScript(ctx, s) { + return ErrNoTriggerManagementPermissions + } + + return svc.triggerManager.UpdateTrigger(ctx, s, t) +} + +func (svc automationTrigger) Delete(ctx context.Context, s *automation.Script, t *automation.Trigger) (err error) { + if err = svc.isValid(ctx, s, t); err != nil { + return + } + + if !svc.ac.CanUpdateAutomationScript(ctx, s) { + return ErrNoTriggerManagementPermissions + } + + return svc.triggerManager.DeleteTrigger(ctx, t) +} + +// Validates trigger (in compose context) +func (svc automationTrigger) isValid(ctx context.Context, s *automation.Script, t *automation.Trigger) error { + if !t.Enabled { + return nil + } + + if t.Resource == "system:mail" { + if !svc.ac.CanCreateAutomationScript(ctx) { + return ErrNoScriptCreatePermissions + } + + if t.IsDeferred() { + // @todo validate condition for deferred triggers + return automation.ErrAutomationTriggerInvalidEvent + } + + switch t.Event { + case "onReceive": + // @todo validate + + default: + return automation.ErrAutomationTriggerInvalidEvent + } + + return nil + } + + return automation.ErrAutomationTriggerInvalidResource + +} diff --git a/system/internal/service/error.go b/system/internal/service/error.go index bf77f2bfc..0564c63a1 100644 --- a/system/internal/service/error.go +++ b/system/internal/service/error.go @@ -9,8 +9,13 @@ type ( ) const ( - ErrInvalidID serviceError = "InvalidID" - ErrNoPermissions serviceError = "NoPermissions" + ErrInvalidID serviceError = "InvalidID" + ErrNoPermissions serviceError = "NoPermissions" + ErrNoGrantPermissions serviceError = "NoGrantPermissions" + ErrNoCreatePermissions serviceError = "NoCreatePermissions" + ErrNoReadPermissions serviceError = "NoReadPermissions" + ErrNoTriggerManagementPermissions serviceError = "NoTriggerManagementPermissions" + ErrNoScriptCreatePermissions serviceError = "NoScriptCreatePermissions" ) func (e serviceError) Error() string { diff --git a/system/internal/service/mailproc.go b/system/internal/service/mailproc.go new file mode 100644 index 000000000..418133635 --- /dev/null +++ b/system/internal/service/mailproc.go @@ -0,0 +1,100 @@ +package service + +import ( + "context" + "io" + "io/ioutil" + "net/mail" + + "go.uber.org/zap" + + "github.com/cortezaproject/corteza-server/system/types" +) + +type ( + mailproc struct { + sr mailprocScriptsRunner + + logger *zap.Logger + } + + mailprocScriptsRunner interface { + OnReceiveMailMessage(ctx context.Context, message *types.MailMessage) (err error) + } +) + +func Mailproc() *mailproc { + return &mailproc{ + sr: DefaultAutomationRunner, + logger: DefaultLogger.Named("mailproc"), + } +} + +// log() returns zap's logger with requestID from current context and fields. +// func (svc mailproc) log(fields ...zapcore.Field) *zap.Logger { +// return logger.AddRequestID(svc.ctx, svc.logger).With(fields...) +// } + +func (svc mailproc) ContentProcessor(ctx context.Context, m io.Reader) error { + if m, err := mailProcMessage(m); err != nil { + return err + } else if err = svc.sr.OnReceiveMailMessage(ctx, m); err != nil { + return err + } + + return nil +} + +func mailProcMessage(r io.Reader) (out *types.MailMessage, err error) { + var ( + aa []*mail.Address + msg *mail.Message + + addrKeys = []string{ + "from", + "to", + "cc", + "bcc", + "reply-to", + } + ) + + if msg, err = mail.ReadMessage(r); err != nil { + return + } + + out = &types.MailMessage{} + + out.Header.Raw = msg.Header + + out.Date, _ = msg.Header.Date() + + for _, key := range addrKeys { + aa, err = msg.Header.AddressList(key) + + if err != nil && err != mail.ErrHeaderNotPresent { + return + } + + if len(aa) > 0 { + switch key { + case "from": + out.Header.From = aa + case "to": + out.Header.To = aa + case "cc": + out.Header.CC = aa + case "bcc": + out.Header.BCC = aa + case "reply-to": + out.Header.ReplyTo = aa + } + } + } + + if out.RawBody, err = ioutil.ReadAll(msg.Body); err != nil { + return + } + + return +} diff --git a/system/internal/service/mailproc_test.go b/system/internal/service/mailproc_test.go new file mode 100644 index 000000000..1195c7f2a --- /dev/null +++ b/system/internal/service/mailproc_test.go @@ -0,0 +1,61 @@ +package service + +import ( + "net/mail" + "reflect" + "strings" + "testing" + "time" + + "github.com/cortezaproject/corteza-server/system/types" +) + +func Test_mailProcMessage(t *testing.T) { + tests := []struct { + name string + input string + wantOut *types.MailMessage + wantErr bool + }{ + // TODO: Add test cases. + {name: "basics", + input: ` +From: +To: +Subject: Customer service contact info +Message-ID: <1234@local.machine.example> + +Ola Corteza! +`, + wantOut: &types.MailMessage{ + Date: time.Time{}, + Header: types.MailMessageHeader{ + From: []*mail.Address{{Address: "sender@testing.cortezaproject.org"}}, + To: []*mail.Address{{Address: "rcpt@testing.cortezaproject.org"}}, + + Raw: map[string][]string{ + "From": []string{""}, + "To": []string{""}, + "Subject": []string{"Customer service contact info"}, + "Message-Id": []string{"<1234@local.machine.example>"}, + }, + }, + RawBody: []byte(`Ola Corteza!`), + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := strings.NewReader(strings.TrimSpace(tt.input)) + gotOut, err := mailProcMessage(input) + + if (err != nil) != tt.wantErr { + t.Errorf("mailProcMessage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotOut, tt.wantOut) { + t.Errorf("mailProcMessage() \ngotOut: %v, \n want: %v", gotOut, tt.wantOut) + } + }) + } +} diff --git a/system/internal/service/service.go b/system/internal/service/service.go index 78d078b93..cad693d35 100644 --- a/system/internal/service/service.go +++ b/system/internal/service/service.go @@ -5,9 +5,14 @@ import ( "go.uber.org/zap" + intAuth "github.com/cortezaproject/corteza-server/internal/auth" "github.com/cortezaproject/corteza-server/internal/permissions" internalSettings "github.com/cortezaproject/corteza-server/internal/settings" + "github.com/cortezaproject/corteza-server/pkg/automation" + "github.com/cortezaproject/corteza-server/pkg/automation/corredor" + "github.com/cortezaproject/corteza-server/pkg/cli/options" "github.com/cortezaproject/corteza-server/system/internal/repository" + "github.com/cortezaproject/corteza-server/system/types" ) type ( @@ -18,20 +23,40 @@ type ( accessControlPermissionServicer Watch(ctx context.Context) } + + Config struct { + Storage options.StorageOpt + Corredor options.CorredorOpt + GRPCClientSystem options.GRPCServerOpt + } ) var ( - DefaultPermissions permissionServicer - DefaultIntSettings internalSettings.Service - DefaultLogger *zap.Logger + // DefaultPermissions Retrieves & stores permissions + DefaultPermissions permissionServicer + + DefaultIntSettings internalSettings.Service + DefaultSettings SettingsService + + // DefaultAccessControl Access control checking DefaultAccessControl *accessControl - DefaultSettings SettingsService + // DefaultAutomationScriptManager manages scripts + DefaultAutomationScriptManager automationScript + + // DefaultAutomationTriggerManager manages triggerManager + DefaultAutomationTriggerManager automationTrigger + + // DefaultAutomationRunner runs automation scripts by listening to triggerManager and invoking Corredor service + DefaultAutomationRunner automationRunner + DefaultAuthNotification AuthNotificationService DefaultAuthSettings AuthSettings + DefaultSink *sink + DefaultAuth AuthService DefaultUser UserService DefaultRole RoleService @@ -39,7 +64,7 @@ var ( DefaultApplication ApplicationService ) -func Init(ctx context.Context, log *zap.Logger) (err error) { +func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { DefaultLogger = log.Named("service") DefaultIntSettings = internalSettings.NewService(internalSettings.NewRepository(repository.DB(ctx), "sys_settings")) @@ -65,9 +90,64 @@ func Init(ctx context.Context, log *zap.Logger) (err error) { DefaultAuthNotification = AuthNotification(ctx) DefaultAuth = Auth(ctx) + // ias: Internal Automatinon Service + // handles script & trigger management & keeping runnables cripts in internal cache + ias := automation.Service(automation.AutomationServiceConfig{ + Logger: DefaultLogger, + DbTablePrefix: "sys", + DB: repository.DB(ctx), + TokenMaker: func(ctx context.Context, userID uint64) (jwt string, err error) { + var u *types.User + + ctx = intAuth.SetSuperUserContext(ctx) + if u, err = DefaultUser.FindByID(userID); err != nil { + return + } else if err = DefaultAuth.LoadRoleMemberships(u); err != nil { + return + } + + return intAuth.DefaultJwtHandler.Encode(u), nil + }, + }) + + // Pass automation manager to + DefaultAutomationTriggerManager = AutomationTrigger(ias) + DefaultAutomationScriptManager = AutomationScript(ias) + + { + var scriptRunnerClient corredor.ScriptRunnerClient + + if c.Corredor.Enabled { + conn, err := corredor.NewConnection(ctx, c.Corredor, DefaultLogger) + + log.Info("initializing corredor connection", zap.String("addr", c.Corredor.Addr), zap.Error(err)) + if err != nil { + return err + } + + scriptRunnerClient = corredor.NewScriptRunnerClient(conn) + } + + DefaultAutomationRunner = AutomationRunner( + AutomationRunnerOpt{ + ApiBaseURLSystem: c.Corredor.ApiBaseURLSystem, + ApiBaseURLMessaging: c.Corredor.ApiBaseURLMessaging, + ApiBaseURLCompose: c.Corredor.ApiBaseURLCompose, + }, + ias, + scriptRunnerClient, + ) + } + + DefaultSink = Sink() + return } func Watchers(ctx context.Context) { + // Reloading automation scripts on change + DefaultAutomationRunner.Watch(ctx) + + // Reloading permissions on change DefaultPermissions.Watch(ctx) } diff --git a/system/internal/service/sink.go b/system/internal/service/sink.go new file mode 100644 index 000000000..65c197654 --- /dev/null +++ b/system/internal/service/sink.go @@ -0,0 +1,53 @@ +package service + +import ( + "context" + "io" + "strings" + + "go.uber.org/zap" +) + +type ( + sink struct { + // processors + proc map[string]sinkContentProc + + logger *zap.Logger + } + + sinkContentProc interface { + ContentProcessor(context.Context, io.Reader) error + } +) + +const ( + ErrSinkContentTypeUnsupported serviceError = "SinkUnsupportedContentType" + ErrSinkContentProcessingFailed serviceError = "SinkProcessFailed" + + SinkContentTypeMail = "message/rfc822" +) + +func Sink() *sink { + return &sink{ + logger: DefaultLogger, + proc: map[string]sinkContentProc{ + SinkContentTypeMail: Mailproc(), + }, + } +} + +// Finds appropriate sink processor +func (svc *sink) Process(ctx context.Context, contentType string, r io.Reader) (err error) { + switch strings.ToLower(contentType) { + case SinkContentTypeMail, "rfc822", "email", "mail": + if err = svc.proc[SinkContentTypeMail].ContentProcessor(ctx, r); err != nil { + return ErrSinkContentProcessingFailed + } + + default: + return ErrSinkContentTypeUnsupported + } + + return +} diff --git a/system/proto/mail_message.go b/system/proto/mail_message.go new file mode 100644 index 000000000..72462344c --- /dev/null +++ b/system/proto/mail_message.go @@ -0,0 +1,53 @@ +package proto + +import ( + mail2 "net/mail" + + "github.com/golang/protobuf/ptypes/timestamp" + + "github.com/cortezaproject/corteza-server/system/types" +) + +func NewMailMessage(mail *types.MailMessage) *MailMessage { + if mail == nil { + return nil + } + + addrConv := func(aa []*mail2.Address) []*MailMessage_Header_MailAddress { + out := make([]*MailMessage_Header_MailAddress, len(aa)) + + for i, a := range aa { + out[i] = &MailMessage_Header_MailAddress{ + Address: a.Address, + Name: a.Name, + } + } + + return out + } + + hConv := func(hh mail2.Header) map[string]*MailMessage_Header_HeaderValues { + out := make(map[string]*MailMessage_Header_HeaderValues, len(hh)) + + for k, vv := range hh { + out[k] = &MailMessage_Header_HeaderValues{Values: vv} + } + + return out + } + + var p = &MailMessage{ + Header: &MailMessage_Header{ + Date: ×tamp.Timestamp{Seconds: mail.Date.Unix()}, + To: addrConv(mail.Header.To), + Cc: addrConv(mail.Header.CC), + Bcc: addrConv(mail.Header.BCC), + From: addrConv(mail.Header.From), + ReplyTo: addrConv(mail.Header.ReplyTo), + Raw: hConv(mail.Header.Raw), + }, + RawBody: mail.RawBody, + } + + return p +} diff --git a/system/proto/mail_message.pb.go b/system/proto/mail_message.pb.go new file mode 100644 index 000000000..f00fedbe8 --- /dev/null +++ b/system/proto/mail_message.pb.go @@ -0,0 +1,286 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: mail_message.proto + +package proto + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + timestamp "github.com/golang/protobuf/ptypes/timestamp" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type MailMessage struct { + Header *MailMessage_Header `protobuf:"bytes,6,opt,name=header,proto3" json:"header,omitempty"` + RawBody []byte `protobuf:"bytes,15,opt,name=rawBody,proto3" json:"rawBody,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *MailMessage) Reset() { *m = MailMessage{} } +func (m *MailMessage) String() string { return proto.CompactTextString(m) } +func (*MailMessage) ProtoMessage() {} +func (*MailMessage) Descriptor() ([]byte, []int) { + return fileDescriptor_d142ecb16d253662, []int{0} +} + +func (m *MailMessage) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_MailMessage.Unmarshal(m, b) +} +func (m *MailMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_MailMessage.Marshal(b, m, deterministic) +} +func (m *MailMessage) XXX_Merge(src proto.Message) { + xxx_messageInfo_MailMessage.Merge(m, src) +} +func (m *MailMessage) XXX_Size() int { + return xxx_messageInfo_MailMessage.Size(m) +} +func (m *MailMessage) XXX_DiscardUnknown() { + xxx_messageInfo_MailMessage.DiscardUnknown(m) +} + +var xxx_messageInfo_MailMessage proto.InternalMessageInfo + +func (m *MailMessage) GetHeader() *MailMessage_Header { + if m != nil { + return m.Header + } + return nil +} + +func (m *MailMessage) GetRawBody() []byte { + if m != nil { + return m.RawBody + } + return nil +} + +// All typified data (date, to, cc, bcc...) +// is extracted and parsed +// +// Simple string values (subject, message-id, ...) +// are kept under Raw +// +type MailMessage_Header struct { + Date *timestamp.Timestamp `protobuf:"bytes,1,opt,name=date,proto3" json:"date,omitempty"` + To []*MailMessage_Header_MailAddress `protobuf:"bytes,2,rep,name=to,proto3" json:"to,omitempty"` + Cc []*MailMessage_Header_MailAddress `protobuf:"bytes,3,rep,name=cc,proto3" json:"cc,omitempty"` + Bcc []*MailMessage_Header_MailAddress `protobuf:"bytes,4,rep,name=bcc,proto3" json:"bcc,omitempty"` + From []*MailMessage_Header_MailAddress `protobuf:"bytes,5,rep,name=from,proto3" json:"from,omitempty"` + ReplyTo []*MailMessage_Header_MailAddress `protobuf:"bytes,6,rep,name=replyTo,proto3" json:"replyTo,omitempty"` + Raw map[string]*MailMessage_Header_HeaderValues `protobuf:"bytes,15,rep,name=raw,proto3" json:"raw,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *MailMessage_Header) Reset() { *m = MailMessage_Header{} } +func (m *MailMessage_Header) String() string { return proto.CompactTextString(m) } +func (*MailMessage_Header) ProtoMessage() {} +func (*MailMessage_Header) Descriptor() ([]byte, []int) { + return fileDescriptor_d142ecb16d253662, []int{0, 0} +} + +func (m *MailMessage_Header) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_MailMessage_Header.Unmarshal(m, b) +} +func (m *MailMessage_Header) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_MailMessage_Header.Marshal(b, m, deterministic) +} +func (m *MailMessage_Header) XXX_Merge(src proto.Message) { + xxx_messageInfo_MailMessage_Header.Merge(m, src) +} +func (m *MailMessage_Header) XXX_Size() int { + return xxx_messageInfo_MailMessage_Header.Size(m) +} +func (m *MailMessage_Header) XXX_DiscardUnknown() { + xxx_messageInfo_MailMessage_Header.DiscardUnknown(m) +} + +var xxx_messageInfo_MailMessage_Header proto.InternalMessageInfo + +func (m *MailMessage_Header) GetDate() *timestamp.Timestamp { + if m != nil { + return m.Date + } + return nil +} + +func (m *MailMessage_Header) GetTo() []*MailMessage_Header_MailAddress { + if m != nil { + return m.To + } + return nil +} + +func (m *MailMessage_Header) GetCc() []*MailMessage_Header_MailAddress { + if m != nil { + return m.Cc + } + return nil +} + +func (m *MailMessage_Header) GetBcc() []*MailMessage_Header_MailAddress { + if m != nil { + return m.Bcc + } + return nil +} + +func (m *MailMessage_Header) GetFrom() []*MailMessage_Header_MailAddress { + if m != nil { + return m.From + } + return nil +} + +func (m *MailMessage_Header) GetReplyTo() []*MailMessage_Header_MailAddress { + if m != nil { + return m.ReplyTo + } + return nil +} + +func (m *MailMessage_Header) GetRaw() map[string]*MailMessage_Header_HeaderValues { + if m != nil { + return m.Raw + } + return nil +} + +type MailMessage_Header_MailAddress struct { + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *MailMessage_Header_MailAddress) Reset() { *m = MailMessage_Header_MailAddress{} } +func (m *MailMessage_Header_MailAddress) String() string { return proto.CompactTextString(m) } +func (*MailMessage_Header_MailAddress) ProtoMessage() {} +func (*MailMessage_Header_MailAddress) Descriptor() ([]byte, []int) { + return fileDescriptor_d142ecb16d253662, []int{0, 0, 0} +} + +func (m *MailMessage_Header_MailAddress) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_MailMessage_Header_MailAddress.Unmarshal(m, b) +} +func (m *MailMessage_Header_MailAddress) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_MailMessage_Header_MailAddress.Marshal(b, m, deterministic) +} +func (m *MailMessage_Header_MailAddress) XXX_Merge(src proto.Message) { + xxx_messageInfo_MailMessage_Header_MailAddress.Merge(m, src) +} +func (m *MailMessage_Header_MailAddress) XXX_Size() int { + return xxx_messageInfo_MailMessage_Header_MailAddress.Size(m) +} +func (m *MailMessage_Header_MailAddress) XXX_DiscardUnknown() { + xxx_messageInfo_MailMessage_Header_MailAddress.DiscardUnknown(m) +} + +var xxx_messageInfo_MailMessage_Header_MailAddress proto.InternalMessageInfo + +func (m *MailMessage_Header_MailAddress) GetAddress() string { + if m != nil { + return m.Address + } + return "" +} + +func (m *MailMessage_Header_MailAddress) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +type MailMessage_Header_HeaderValues struct { + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *MailMessage_Header_HeaderValues) Reset() { *m = MailMessage_Header_HeaderValues{} } +func (m *MailMessage_Header_HeaderValues) String() string { return proto.CompactTextString(m) } +func (*MailMessage_Header_HeaderValues) ProtoMessage() {} +func (*MailMessage_Header_HeaderValues) Descriptor() ([]byte, []int) { + return fileDescriptor_d142ecb16d253662, []int{0, 0, 1} +} + +func (m *MailMessage_Header_HeaderValues) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_MailMessage_Header_HeaderValues.Unmarshal(m, b) +} +func (m *MailMessage_Header_HeaderValues) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_MailMessage_Header_HeaderValues.Marshal(b, m, deterministic) +} +func (m *MailMessage_Header_HeaderValues) XXX_Merge(src proto.Message) { + xxx_messageInfo_MailMessage_Header_HeaderValues.Merge(m, src) +} +func (m *MailMessage_Header_HeaderValues) XXX_Size() int { + return xxx_messageInfo_MailMessage_Header_HeaderValues.Size(m) +} +func (m *MailMessage_Header_HeaderValues) XXX_DiscardUnknown() { + xxx_messageInfo_MailMessage_Header_HeaderValues.DiscardUnknown(m) +} + +var xxx_messageInfo_MailMessage_Header_HeaderValues proto.InternalMessageInfo + +func (m *MailMessage_Header_HeaderValues) GetValues() []string { + if m != nil { + return m.Values + } + return nil +} + +func init() { + proto.RegisterType((*MailMessage)(nil), "system.MailMessage") + proto.RegisterType((*MailMessage_Header)(nil), "system.MailMessage.Header") + proto.RegisterMapType((map[string]*MailMessage_Header_HeaderValues)(nil), "system.MailMessage.Header.RawEntry") + proto.RegisterType((*MailMessage_Header_MailAddress)(nil), "system.MailMessage.Header.MailAddress") + proto.RegisterType((*MailMessage_Header_HeaderValues)(nil), "system.MailMessage.Header.HeaderValues") +} + +func init() { proto.RegisterFile("mail_message.proto", fileDescriptor_d142ecb16d253662) } + +var fileDescriptor_d142ecb16d253662 = []byte{ + // 377 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x92, 0xcd, 0x8a, 0xdb, 0x30, + 0x14, 0x85, 0xf1, 0x7f, 0x72, 0x1d, 0x48, 0xd0, 0xa2, 0x08, 0x6f, 0x6a, 0x5a, 0x48, 0xbd, 0x72, + 0xc0, 0xa5, 0x25, 0xa4, 0x14, 0xda, 0x40, 0xa1, 0x14, 0xb2, 0x11, 0xa1, 0x8b, 0xd9, 0x04, 0xc5, + 0x56, 0x32, 0x61, 0xec, 0xc8, 0x48, 0xca, 0x04, 0xbf, 0xe1, 0xbc, 0xc4, 0xbc, 0xcb, 0x60, 0xc9, + 0x86, 0x6c, 0x26, 0xe0, 0x95, 0xcf, 0xbd, 0x9c, 0xef, 0xe8, 0xc0, 0x35, 0xa0, 0x8a, 0x9e, 0xca, + 0x5d, 0xc5, 0xa4, 0xa4, 0x47, 0x96, 0xd6, 0x82, 0x2b, 0x8e, 0x7c, 0xd9, 0x48, 0xc5, 0xaa, 0xe8, + 0xe3, 0x91, 0xf3, 0x63, 0xc9, 0x16, 0x7a, 0xbb, 0xbf, 0x1c, 0x16, 0xea, 0x54, 0x31, 0xa9, 0x68, + 0x55, 0x1b, 0xe3, 0xa7, 0x17, 0x0f, 0xc2, 0x0d, 0x3d, 0x95, 0x1b, 0x83, 0xa3, 0x0c, 0xfc, 0x47, + 0x46, 0x0b, 0x26, 0xb0, 0x1f, 0x5b, 0x49, 0x98, 0x45, 0xa9, 0x49, 0x4a, 0x6f, 0x4c, 0xe9, 0x5f, + 0xed, 0x20, 0x9d, 0x13, 0x61, 0x08, 0x04, 0xbd, 0xae, 0x79, 0xd1, 0xe0, 0x69, 0x6c, 0x25, 0x13, + 0xd2, 0x8f, 0xd1, 0xab, 0x0b, 0xbe, 0x31, 0xa3, 0x14, 0xdc, 0x82, 0x2a, 0x86, 0xad, 0x2e, 0xd6, + 0x14, 0x4b, 0xfb, 0x62, 0xe9, 0xb6, 0x2f, 0x46, 0xb4, 0x0f, 0x7d, 0x07, 0x5b, 0x71, 0x6c, 0xc7, + 0x4e, 0x12, 0x66, 0xf3, 0xf7, 0x4b, 0xe8, 0xd5, 0xef, 0xa2, 0x10, 0x4c, 0x4a, 0x62, 0x2b, 0xde, + 0x72, 0x79, 0x8e, 0x9d, 0x61, 0x5c, 0x9e, 0xa3, 0x25, 0x38, 0xfb, 0x3c, 0xc7, 0xee, 0x20, 0xb0, + 0x45, 0xd0, 0x0a, 0xdc, 0x83, 0xe0, 0x15, 0xf6, 0x06, 0xa1, 0x9a, 0x41, 0xbf, 0x20, 0x10, 0xac, + 0x2e, 0x9b, 0x2d, 0xc7, 0xfe, 0x20, 0xbc, 0xc7, 0xd0, 0x37, 0x70, 0x04, 0xbd, 0xe2, 0xa9, 0xa6, + 0x3f, 0xdf, 0xa1, 0x09, 0xbd, 0xfe, 0x39, 0x2b, 0xd1, 0x90, 0xd6, 0x1f, 0xfd, 0x30, 0x67, 0xef, + 0xe2, 0xda, 0x13, 0x52, 0x23, 0xf5, 0x81, 0xc6, 0xa4, 0x1f, 0x11, 0x02, 0xf7, 0x4c, 0x2b, 0x86, + 0x6d, 0xbd, 0xd6, 0x3a, 0x9a, 0xc3, 0xc4, 0x84, 0xfe, 0xa7, 0xe5, 0x85, 0x49, 0xf4, 0x01, 0xfc, + 0x67, 0xad, 0xb0, 0x15, 0x3b, 0xc9, 0x98, 0x74, 0x53, 0xb4, 0x83, 0x51, 0xff, 0x2a, 0x9a, 0x81, + 0xf3, 0xc4, 0x9a, 0x2e, 0xbd, 0x95, 0xe8, 0x27, 0x78, 0xda, 0xa7, 0xa3, 0xc3, 0xec, 0xcb, 0x9d, + 0xee, 0xb7, 0xaf, 0x11, 0x43, 0xad, 0xec, 0xa5, 0xf5, 0xcf, 0x1d, 0x05, 0xb3, 0x29, 0xf1, 0x6a, + 0x2a, 0x94, 0x5c, 0x07, 0x0f, 0x9e, 0xf9, 0x9b, 0x7c, 0xfd, 0xf9, 0xfa, 0x16, 0x00, 0x00, 0xff, + 0xff, 0x16, 0x0c, 0x9f, 0x19, 0x19, 0x03, 0x00, 0x00, +} diff --git a/system/rest/automation_script.go b/system/rest/automation_script.go new file mode 100644 index 000000000..9e4ac32e8 --- /dev/null +++ b/system/rest/automation_script.go @@ -0,0 +1,180 @@ +package rest + +import ( + "context" + + "github.com/pkg/errors" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/pkg/automation" + "github.com/cortezaproject/corteza-server/pkg/rh" + "github.com/cortezaproject/corteza-server/system/internal/service" + "github.com/cortezaproject/corteza-server/system/rest/request" +) + +var _ = errors.Wrap + +type ( + automationScriptPayload struct { + *automation.Script + + CanGrant bool `json:"canGrant"` + CanUpdate bool `json:"canUpdate"` + CanDelete bool `json:"canDelete"` + CanSetRunner bool `json:"canSetRunner"` + CanSetAsAsync bool `json:"canSetAsAsync"` + CanSetAsCritical bool `json:"canAsCritical"` + } + + automationScriptSetPayload struct { + Filter automation.ScriptFilter `json:"filter"` + Set []*automationScriptPayload `json:"set"` + } + + automationScriptRunnablePayload struct { + Set []*automationScriptRunnable `json:"set"` + } + + automationScriptRunnable struct { + ScriptID uint64 `json:"scriptID,string"` + Name string `json:"name"` + Events map[string][]string `json:"events"` + Source string `json:"source,omitempty"` + Async bool `json:"async"` + } + + AutomationScript struct { + scripts automationScriptService + runner automationScriptRunner + ac automationScriptAccessController + } + + automationScriptService interface { + FindByID(context.Context, uint64) (*automation.Script, error) + Find(context.Context, automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) + Create(context.Context, *automation.Script) error + Update(context.Context, *automation.Script) error + Delete(context.Context, uint64) error + } + + automationScriptRunner interface { + RecordScriptTester(context.Context, string, interface{}) error + } + + automationScriptAccessController interface { + CanGrant(context.Context) bool + + CanUpdateAutomationScript(context.Context, *automation.Script) bool + CanDeleteAutomationScript(context.Context, *automation.Script) bool + } +) + +func (AutomationScript) New() *AutomationScript { + return &AutomationScript{ + scripts: service.DefaultAutomationScriptManager, + runner: service.DefaultAutomationRunner, + ac: service.DefaultAccessControl, + } +} + +func (ctrl AutomationScript) List(ctx context.Context, r *request.AutomationScriptList) (interface{}, error) { + set, filter, err := ctrl.scripts.Find(ctx, automation.ScriptFilter{ + Query: r.Query, + Resource: r.Resource, + + IncDeleted: false, + PageFilter: rh.Paging(r.Page, r.PerPage), + }) + + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl AutomationScript) Create(ctx context.Context, r *request.AutomationScriptCreate) (interface{}, error) { + var ( + script = &automation.Script{ + Name: r.Name, + SourceRef: r.SourceRef, + Source: r.Source, + Async: r.Async, + RunAs: r.RunAs, + Timeout: r.Timeout, + Critical: r.Critical, + Enabled: r.Enabled, + } + ) + + script.AddTrigger(automation.STMS_FRESH, r.Triggers...) + + return ctrl.makePayload(ctx, script, ctrl.scripts.Create(ctx, script)) +} + +func (ctrl AutomationScript) Read(ctx context.Context, r *request.AutomationScriptRead) (interface{}, error) { + script, err := ctrl.scripts.FindByID(ctx, r.ScriptID) + return ctrl.makePayload(ctx, script, err) +} + +func (ctrl AutomationScript) Update(ctx context.Context, r *request.AutomationScriptUpdate) (interface{}, error) { + mod := &automation.Script{ + ID: r.ScriptID, + Name: r.Name, + SourceRef: r.SourceRef, + Source: r.Source, + Async: r.Async, + RunAs: r.RunAs, + Timeout: r.Timeout, + Critical: r.Critical, + Enabled: r.Enabled, + } + + mod.AddTrigger(automation.STMS_UPDATE, r.Triggers...) + + return ctrl.makePayload(ctx, mod, ctrl.scripts.Update(ctx, mod)) +} + +func (ctrl AutomationScript) Delete(ctx context.Context, r *request.AutomationScriptDelete) (interface{}, error) { + return resputil.OK(), ctrl.scripts.Delete(ctx, r.ScriptID) +} + +func (ctrl AutomationScript) Test(ctx context.Context, r *request.AutomationScriptTest) (interface{}, error) { + var ( + err error + ) + + if err = ctrl.runner.RecordScriptTester(ctx, r.Source, r.Payload); err != nil { + return nil, err + } + + return r.Payload, err +} + +func (ctrl AutomationScript) makePayload(ctx context.Context, s *automation.Script, err error) (*automationScriptPayload, error) { + if err != nil || s == nil { + return nil, err + } + + return &automationScriptPayload{ + Script: s, + + CanGrant: ctrl.ac.CanGrant(ctx), + CanUpdate: ctrl.ac.CanUpdateAutomationScript(ctx, s), + CanDelete: ctrl.ac.CanDeleteAutomationScript(ctx, s), + + CanSetRunner: ctrl.ac.CanGrant(ctx), + CanSetAsCritical: true, + CanSetAsAsync: true, + }, nil +} + +func (ctrl AutomationScript) makeFilterPayload(ctx context.Context, nn automation.ScriptSet, f automation.ScriptFilter, err error) (*automationScriptSetPayload, error) { + if err != nil { + return nil, err + } + + modp := &automationScriptSetPayload{Filter: f, Set: make([]*automationScriptPayload, len(nn))} + + for i := range nn { + modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return modp, nil +} diff --git a/system/rest/automation_trigger.go b/system/rest/automation_trigger.go new file mode 100644 index 000000000..cd2e5a4a9 --- /dev/null +++ b/system/rest/automation_trigger.go @@ -0,0 +1,166 @@ +package rest + +import ( + "context" + + "github.com/pkg/errors" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/pkg/automation" + "github.com/cortezaproject/corteza-server/pkg/rh" + "github.com/cortezaproject/corteza-server/system/internal/service" + "github.com/cortezaproject/corteza-server/system/rest/request" +) + +var _ = errors.Wrap + +type ( + automationTriggerPayload struct { + *automation.Trigger + + CanRun bool `json:"canRun"` + } + + automationTriggerSetPayload struct { + Filter automation.TriggerFilter `json:"filter"` + Set []*automationTriggerPayload `json:"set"` + } + + AutomationTrigger struct { + triggers automationTriggerService + scripts automationScriptFinderService + + ac automationTriggerAccessController + } + + automationTriggerService interface { + FindByID(context.Context, uint64) (*automation.Trigger, error) + Find(context.Context, automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error) + Create(context.Context, *automation.Script, *automation.Trigger) error + Update(context.Context, *automation.Script, *automation.Trigger) error + Delete(context.Context, *automation.Script, *automation.Trigger) error + } + + automationScriptFinderService interface { + FindByID(context.Context, uint64) (*automation.Script, error) + } + + automationTriggerAccessController interface { + CanGrant(context.Context) bool + + CanRunAutomationTrigger(context.Context, *automation.Trigger) bool + } +) + +func (AutomationTrigger) New() *AutomationTrigger { + return &AutomationTrigger{ + scripts: service.DefaultAutomationScriptManager, + triggers: service.DefaultAutomationTriggerManager, + ac: service.DefaultAccessControl, + } +} + +func (ctrl AutomationTrigger) List(ctx context.Context, r *request.AutomationTriggerList) (interface{}, error) { + set, filter, err := ctrl.triggers.Find(ctx, automation.TriggerFilter{ + Resource: r.Resource, + Event: r.Event, + ScriptID: r.ScriptID, + + IncDeleted: false, + PageFilter: rh.Paging(r.Page, r.PerPage), + }) + + return ctrl.makeFilterPayload(ctx, set, filter, err) +} + +func (ctrl AutomationTrigger) Create(ctx context.Context, r *request.AutomationTriggerCreate) (interface{}, error) { + s, _, err := ctrl.loadCombo(ctx, r.ScriptID, 0) + if err != nil { + return nil, errors.Wrap(err, "can not create trigger") + } + + var ( + t = &automation.Trigger{ + Event: r.Event, + Resource: r.Resource, + Condition: r.Condition, + ScriptID: s.ID, + Enabled: r.Enabled, + } + ) + + return ctrl.makePayload(ctx, t, ctrl.triggers.Create(ctx, s, t)) +} + +func (ctrl AutomationTrigger) Read(ctx context.Context, r *request.AutomationTriggerRead) (interface{}, error) { + _, t, err := ctrl.loadCombo(ctx, r.ScriptID, 0) + if err != nil { + return nil, errors.Wrap(err, "can not read trigger") + } + + return ctrl.makePayload(ctx, t, err) +} + +func (ctrl AutomationTrigger) Update(ctx context.Context, r *request.AutomationTriggerUpdate) (interface{}, error) { + s, t, err := ctrl.loadCombo(ctx, r.ScriptID, r.TriggerID) + if err != nil { + return nil, errors.Wrap(err, "can not update trigger") + } + + t.Event = r.Event + t.Resource = r.Resource + t.Condition = r.Condition + t.ScriptID = r.ScriptID + t.Enabled = r.Enabled + + return ctrl.makePayload(ctx, t, ctrl.triggers.Update(ctx, s, t)) +} + +func (ctrl AutomationTrigger) Delete(ctx context.Context, r *request.AutomationTriggerDelete) (interface{}, error) { + s, t, err := ctrl.loadCombo(ctx, r.ScriptID, r.TriggerID) + if err != nil { + return nil, errors.Wrap(err, "can not update trigger") + } + + return resputil.OK(), ctrl.triggers.Delete(ctx, s, t) +} + +func (ctrl AutomationTrigger) loadCombo(ctx context.Context, scriptID, triggerID uint64) (s *automation.Script, t *automation.Trigger, err error) { + if triggerID > 0 { + t, err = ctrl.triggers.FindByID(ctx, triggerID) + return + } + + if scriptID > 0 { + s, err = ctrl.scripts.FindByID(ctx, scriptID) + return + } + + return +} + +func (ctrl AutomationTrigger) makePayload(ctx context.Context, t *automation.Trigger, err error) (*automationTriggerPayload, error) { + if err != nil || t == nil { + return nil, err + } + + return &automationTriggerPayload{ + Trigger: t, + + CanRun: ctrl.ac.CanRunAutomationTrigger(ctx, t), + }, nil +} + +func (ctrl AutomationTrigger) makeFilterPayload(ctx context.Context, nn automation.TriggerSet, f automation.TriggerFilter, err error) (*automationTriggerSetPayload, error) { + if err != nil { + return nil, err + } + + modp := &automationTriggerSetPayload{Filter: f, Set: make([]*automationTriggerPayload, len(nn))} + + for i := range nn { + modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return modp, nil +} diff --git a/system/rest/handlers/automation_script.go b/system/rest/handlers/automation_script.go new file mode 100644 index 000000000..5ca877347 --- /dev/null +++ b/system/rest/handlers/automation_script.go @@ -0,0 +1,185 @@ +package handlers + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `automation_script.go`, `automation_script.util.go` or `automation_script_test.go` to + implement your API calls, helper functions and tests. The file `automation_script.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + + "net/http" + + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/cortezaproject/corteza-server/system/rest/request" +) + +// Internal API interface +type AutomationScriptAPI interface { + List(context.Context, *request.AutomationScriptList) (interface{}, error) + Create(context.Context, *request.AutomationScriptCreate) (interface{}, error) + Read(context.Context, *request.AutomationScriptRead) (interface{}, error) + Update(context.Context, *request.AutomationScriptUpdate) (interface{}, error) + Delete(context.Context, *request.AutomationScriptDelete) (interface{}, error) + Test(context.Context, *request.AutomationScriptTest) (interface{}, error) +} + +// HTTP API interface +type AutomationScript struct { + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) + Test func(http.ResponseWriter, *http.Request) +} + +func NewAutomationScript(h AutomationScriptAPI) *AutomationScript { + return &AutomationScript{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationScriptList() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationScript.List", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.List(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationScript.List", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationScript.List", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationScriptCreate() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationScript.Create", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Create(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationScript.Create", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationScript.Create", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Read: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationScriptRead() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationScript.Read", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Read(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationScript.Read", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationScript.Read", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Update: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationScriptUpdate() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationScript.Update", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Update(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationScript.Update", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationScript.Update", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Delete: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationScriptDelete() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationScript.Delete", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Delete(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationScript.Delete", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationScript.Delete", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Test: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationScriptTest() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationScript.Test", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Test(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationScript.Test", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationScript.Test", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h AutomationScript) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/automation/script/", h.List) + r.Post("/automation/script/", h.Create) + r.Get("/automation/script/{scriptID}", h.Read) + r.Post("/automation/script/{scriptID}", h.Update) + r.Delete("/automation/script/{scriptID}", h.Delete) + r.Post("/automation/script/test", h.Test) + }) +} diff --git a/system/rest/handlers/automation_trigger.go b/system/rest/handlers/automation_trigger.go new file mode 100644 index 000000000..4aa61c3a3 --- /dev/null +++ b/system/rest/handlers/automation_trigger.go @@ -0,0 +1,162 @@ +package handlers + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `automation_trigger.go`, `automation_trigger.util.go` or `automation_trigger_test.go` to + implement your API calls, helper functions and tests. The file `automation_trigger.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "context" + + "net/http" + + "github.com/go-chi/chi" + "github.com/titpetric/factory/resputil" + + "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/cortezaproject/corteza-server/system/rest/request" +) + +// Internal API interface +type AutomationTriggerAPI interface { + List(context.Context, *request.AutomationTriggerList) (interface{}, error) + Create(context.Context, *request.AutomationTriggerCreate) (interface{}, error) + Read(context.Context, *request.AutomationTriggerRead) (interface{}, error) + Update(context.Context, *request.AutomationTriggerUpdate) (interface{}, error) + Delete(context.Context, *request.AutomationTriggerDelete) (interface{}, error) +} + +// HTTP API interface +type AutomationTrigger struct { + List func(http.ResponseWriter, *http.Request) + Create func(http.ResponseWriter, *http.Request) + Read func(http.ResponseWriter, *http.Request) + Update func(http.ResponseWriter, *http.Request) + Delete func(http.ResponseWriter, *http.Request) +} + +func NewAutomationTrigger(h AutomationTriggerAPI) *AutomationTrigger { + return &AutomationTrigger{ + List: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerList() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.List", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.List(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.List", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.List", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Create: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerCreate() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.Create", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Create(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.Create", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.Create", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Read: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerRead() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.Read", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Read(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.Read", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.Read", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Update: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerUpdate() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.Update", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Update(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.Update", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.Update", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + Delete: func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + params := request.NewAutomationTriggerDelete() + if err := params.Fill(r); err != nil { + logger.LogParamError("AutomationTrigger.Delete", r, err) + resputil.JSON(w, err) + return + } + + value, err := h.Delete(r.Context(), params) + if err != nil { + logger.LogControllerError("AutomationTrigger.Delete", r, err, params.Auditable()) + resputil.JSON(w, err) + return + } + logger.LogControllerCall("AutomationTrigger.Delete", r, params.Auditable()) + if !serveHTTP(value, w, r) { + resputil.JSON(w, value) + } + }, + } +} + +func (h AutomationTrigger) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) { + r.Group(func(r chi.Router) { + r.Use(middlewares...) + r.Get("/automation/script/{scriptID}/trigger/", h.List) + r.Post("/automation/script/{scriptID}/trigger/", h.Create) + r.Get("/automation/script/{scriptID}/trigger/{triggerID}", h.Read) + r.Post("/automation/script/{scriptID}/trigger/{triggerID}", h.Update) + r.Delete("/automation/script/{scriptID}/trigger/{triggerID}", h.Delete) + }) +} diff --git a/system/rest/request/automation_script.go b/system/rest/request/automation_script.go new file mode 100644 index 000000000..b081d663a --- /dev/null +++ b/system/rest/request/automation_script.go @@ -0,0 +1,449 @@ +package request + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `automation_script.go`, `automation_script.util.go` or `automation_script_test.go` to + implement your API calls, helper functions and tests. The file `automation_script.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "io" + "strings" + + "encoding/json" + "mime/multipart" + "net/http" + + "github.com/go-chi/chi" + "github.com/pkg/errors" + + "github.com/cortezaproject/corteza-server/pkg/automation" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// AutomationScript list request parameters +type AutomationScriptList struct { + Query string + Resource string + IncDeleted bool + Page uint + PerPage uint +} + +func NewAutomationScriptList() *AutomationScriptList { + return &AutomationScriptList{} +} + +func (r AutomationScriptList) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["query"] = r.Query + out["resource"] = r.Resource + out["incDeleted"] = r.IncDeleted + out["page"] = r.Page + out["perPage"] = r.PerPage + + return out +} + +func (r *AutomationScriptList) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["query"]; ok { + r.Query = val + } + if val, ok := get["resource"]; ok { + r.Resource = val + } + if val, ok := get["incDeleted"]; ok { + r.IncDeleted = parseBool(val) + } + if val, ok := get["page"]; ok { + r.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + r.PerPage = parseUint(val) + } + + return err +} + +var _ RequestFiller = NewAutomationScriptList() + +// AutomationScript create request parameters +type AutomationScriptCreate struct { + Name string + SourceRef string + Source string + RunAs uint64 `json:",string"` + Timeout uint + Critical bool + Async bool + Enabled bool + Triggers automation.TriggerSet +} + +func NewAutomationScriptCreate() *AutomationScriptCreate { + return &AutomationScriptCreate{} +} + +func (r AutomationScriptCreate) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["name"] = r.Name + out["sourceRef"] = r.SourceRef + out["source"] = r.Source + out["runAs"] = r.RunAs + out["timeout"] = r.Timeout + out["critical"] = r.Critical + out["async"] = r.Async + out["enabled"] = r.Enabled + out["triggers"] = r.Triggers + + return out +} + +func (r *AutomationScriptCreate) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["name"]; ok { + r.Name = val + } + if val, ok := post["sourceRef"]; ok { + r.SourceRef = val + } + if val, ok := post["source"]; ok { + r.Source = val + } + if val, ok := post["runAs"]; ok { + r.RunAs = parseUInt64(val) + } + if val, ok := post["timeout"]; ok { + r.Timeout = parseUint(val) + } + if val, ok := post["critical"]; ok { + r.Critical = parseBool(val) + } + if val, ok := post["async"]; ok { + r.Async = parseBool(val) + } + if val, ok := post["enabled"]; ok { + r.Enabled = parseBool(val) + } + + return err +} + +var _ RequestFiller = NewAutomationScriptCreate() + +// AutomationScript read request parameters +type AutomationScriptRead struct { + ScriptID uint64 `json:",string"` +} + +func NewAutomationScriptRead() *AutomationScriptRead { + return &AutomationScriptRead{} +} + +func (r AutomationScriptRead) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["scriptID"] = r.ScriptID + + return out +} + +func (r *AutomationScriptRead) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + + return err +} + +var _ RequestFiller = NewAutomationScriptRead() + +// AutomationScript update request parameters +type AutomationScriptUpdate struct { + ScriptID uint64 `json:",string"` + Name string + SourceRef string + Source string + RunAs uint64 `json:",string"` + Timeout uint + Critical bool + Async bool + Enabled bool + Triggers automation.TriggerSet +} + +func NewAutomationScriptUpdate() *AutomationScriptUpdate { + return &AutomationScriptUpdate{} +} + +func (r AutomationScriptUpdate) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["scriptID"] = r.ScriptID + out["name"] = r.Name + out["sourceRef"] = r.SourceRef + out["source"] = r.Source + out["runAs"] = r.RunAs + out["timeout"] = r.Timeout + out["critical"] = r.Critical + out["async"] = r.Async + out["enabled"] = r.Enabled + out["triggers"] = r.Triggers + + return out +} + +func (r *AutomationScriptUpdate) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + if val, ok := post["name"]; ok { + r.Name = val + } + if val, ok := post["sourceRef"]; ok { + r.SourceRef = val + } + if val, ok := post["source"]; ok { + r.Source = val + } + if val, ok := post["runAs"]; ok { + r.RunAs = parseUInt64(val) + } + if val, ok := post["timeout"]; ok { + r.Timeout = parseUint(val) + } + if val, ok := post["critical"]; ok { + r.Critical = parseBool(val) + } + if val, ok := post["async"]; ok { + r.Async = parseBool(val) + } + if val, ok := post["enabled"]; ok { + r.Enabled = parseBool(val) + } + + return err +} + +var _ RequestFiller = NewAutomationScriptUpdate() + +// AutomationScript delete request parameters +type AutomationScriptDelete struct { + ScriptID uint64 `json:",string"` +} + +func NewAutomationScriptDelete() *AutomationScriptDelete { + return &AutomationScriptDelete{} +} + +func (r AutomationScriptDelete) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["scriptID"] = r.ScriptID + + return out +} + +func (r *AutomationScriptDelete) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + + return err +} + +var _ RequestFiller = NewAutomationScriptDelete() + +// AutomationScript test request parameters +type AutomationScriptTest struct { + Source string + Payload json.RawMessage +} + +func NewAutomationScriptTest() *AutomationScriptTest { + return &AutomationScriptTest{} +} + +func (r AutomationScriptTest) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["source"] = r.Source + out["payload"] = r.Payload + + return out +} + +func (r *AutomationScriptTest) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["source"]; ok { + r.Source = val + } + if val, ok := post["payload"]; ok { + r.Payload = json.RawMessage(val) + } + + return err +} + +var _ RequestFiller = NewAutomationScriptTest() diff --git a/system/rest/request/automation_trigger.go b/system/rest/request/automation_trigger.go new file mode 100644 index 000000000..ebd899825 --- /dev/null +++ b/system/rest/request/automation_trigger.go @@ -0,0 +1,360 @@ +package request + +/* + Hello! This file is auto-generated from `docs/src/spec.json`. + + For development: + In order to update the generated files, edit this file under the location, + add your struct fields, imports, API definitions and whatever you want, and: + + 1. run [spec](https://github.com/titpetric/spec) in the same folder, + 2. run `./_gen.php` in this folder. + + You may edit `automation_trigger.go`, `automation_trigger.util.go` or `automation_trigger_test.go` to + implement your API calls, helper functions and tests. The file `automation_trigger.go` + is only generated the first time, and will not be overwritten if it exists. +*/ + +import ( + "io" + "strings" + + "encoding/json" + "mime/multipart" + "net/http" + + "github.com/go-chi/chi" + "github.com/pkg/errors" +) + +var _ = chi.URLParam +var _ = multipart.FileHeader{} + +// AutomationTrigger list request parameters +type AutomationTriggerList struct { + Resource string + Event string + IncDeleted bool + Page uint + PerPage uint + ScriptID uint64 `json:",string"` +} + +func NewAutomationTriggerList() *AutomationTriggerList { + return &AutomationTriggerList{} +} + +func (r AutomationTriggerList) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["resource"] = r.Resource + out["event"] = r.Event + out["incDeleted"] = r.IncDeleted + out["page"] = r.Page + out["perPage"] = r.PerPage + out["scriptID"] = r.ScriptID + + return out +} + +func (r *AutomationTriggerList) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := get["resource"]; ok { + r.Resource = val + } + if val, ok := get["event"]; ok { + r.Event = val + } + if val, ok := get["incDeleted"]; ok { + r.IncDeleted = parseBool(val) + } + if val, ok := get["page"]; ok { + r.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + r.PerPage = parseUint(val) + } + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + + return err +} + +var _ RequestFiller = NewAutomationTriggerList() + +// AutomationTrigger create request parameters +type AutomationTriggerCreate struct { + Resource string + Event string + Condition string + Enabled bool + ScriptID uint64 `json:",string"` +} + +func NewAutomationTriggerCreate() *AutomationTriggerCreate { + return &AutomationTriggerCreate{} +} + +func (r AutomationTriggerCreate) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["resource"] = r.Resource + out["event"] = r.Event + out["condition"] = r.Condition + out["enabled"] = r.Enabled + out["scriptID"] = r.ScriptID + + return out +} + +func (r *AutomationTriggerCreate) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + if val, ok := post["resource"]; ok { + r.Resource = val + } + if val, ok := post["event"]; ok { + r.Event = val + } + if val, ok := post["condition"]; ok { + r.Condition = val + } + if val, ok := post["enabled"]; ok { + r.Enabled = parseBool(val) + } + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + + return err +} + +var _ RequestFiller = NewAutomationTriggerCreate() + +// AutomationTrigger read request parameters +type AutomationTriggerRead struct { + TriggerID uint64 `json:",string"` + ScriptID uint64 `json:",string"` +} + +func NewAutomationTriggerRead() *AutomationTriggerRead { + return &AutomationTriggerRead{} +} + +func (r AutomationTriggerRead) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["triggerID"] = r.TriggerID + out["scriptID"] = r.ScriptID + + return out +} + +func (r *AutomationTriggerRead) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID")) + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + + return err +} + +var _ RequestFiller = NewAutomationTriggerRead() + +// AutomationTrigger update request parameters +type AutomationTriggerUpdate struct { + TriggerID uint64 `json:",string"` + ScriptID uint64 `json:",string"` + Resource string + Event string + Condition string + Enabled bool +} + +func NewAutomationTriggerUpdate() *AutomationTriggerUpdate { + return &AutomationTriggerUpdate{} +} + +func (r AutomationTriggerUpdate) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["triggerID"] = r.TriggerID + out["scriptID"] = r.ScriptID + out["resource"] = r.Resource + out["event"] = r.Event + out["condition"] = r.Condition + out["enabled"] = r.Enabled + + return out +} + +func (r *AutomationTriggerUpdate) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID")) + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + if val, ok := post["resource"]; ok { + r.Resource = val + } + if val, ok := post["event"]; ok { + r.Event = val + } + if val, ok := post["condition"]; ok { + r.Condition = val + } + if val, ok := post["enabled"]; ok { + r.Enabled = parseBool(val) + } + + return err +} + +var _ RequestFiller = NewAutomationTriggerUpdate() + +// AutomationTrigger delete request parameters +type AutomationTriggerDelete struct { + TriggerID uint64 `json:",string"` + ScriptID uint64 `json:",string"` +} + +func NewAutomationTriggerDelete() *AutomationTriggerDelete { + return &AutomationTriggerDelete{} +} + +func (r AutomationTriggerDelete) Auditable() map[string]interface{} { + var out = map[string]interface{}{} + + out["triggerID"] = r.TriggerID + out["scriptID"] = r.ScriptID + + return out +} + +func (r *AutomationTriggerDelete) Fill(req *http.Request) (err error) { + if strings.ToLower(req.Header.Get("content-type")) == "application/json" { + err = json.NewDecoder(req.Body).Decode(r) + + switch { + case err == io.EOF: + err = nil + case err != nil: + return errors.Wrap(err, "error parsing http request body") + } + } + + if err = req.ParseForm(); err != nil { + return err + } + + get := map[string]string{} + post := map[string]string{} + urlQuery := req.URL.Query() + for name, param := range urlQuery { + get[name] = string(param[0]) + } + postVars := req.Form + for name, param := range postVars { + post[name] = string(param[0]) + } + + r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID")) + r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID")) + + return err +} + +var _ RequestFiller = NewAutomationTriggerDelete() diff --git a/system/rest/router.go b/system/rest/router.go index d353f56a8..32f7a2799 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -4,6 +4,7 @@ import ( "github.com/go-chi/chi" "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/system/internal/service" "github.com/cortezaproject/corteza-server/system/rest/handlers" ) @@ -13,6 +14,13 @@ func MountRoutes(r chi.Router) { r.Group(func(r chi.Router) { handlers.NewAuth((Auth{}).New()).MountRoutes(r) handlers.NewAuthInternal((AuthInternal{}).New()).MountRoutes(r) + + // A special case that, we do not add this through standard request, handlers & controllers + // combo but directly -- we need access to r.Body + r.Handle("/sink", &Sink{ + svc: service.DefaultSink, + sign: auth.DefaultSigner, + }) }) // Protect all _private_ routes @@ -25,5 +33,8 @@ func MountRoutes(r chi.Router) { handlers.NewPermissions(Permissions{}.New()).MountRoutes(r) handlers.NewApplication(Application{}.New()).MountRoutes(r) handlers.NewSettings(Settings{}.New()).MountRoutes(r) + + handlers.NewAutomationScript(AutomationScript{}.New()).MountRoutes(r) + handlers.NewAutomationTrigger(AutomationTrigger{}.New()).MountRoutes(r) }) } diff --git a/system/rest/sink.go b/system/rest/sink.go new file mode 100644 index 000000000..f72be7352 --- /dev/null +++ b/system/rest/sink.go @@ -0,0 +1,89 @@ +package rest + +import ( + "context" + "io" + "net/http" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/cortezaproject/corteza-server/internal/auth" + "github.com/cortezaproject/corteza-server/system/internal/service" +) + +var _ = errors.Wrap + +type Sink struct { + // xxx service.XXXService + svc interface { + Process(context.Context, string, io.Reader) error + } + + sign auth.Signer +} + +func (ctrl *Sink) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + + // What are we getting + part of the signature + contentType = r.URL.Query().Get("content-type") + + // + sign = r.URL.Query().Get("sign") + origin = r.URL.Query().Get("origin") + expires = r.URL.Query().Get("expires") + method = strings.ToUpper(r.Method) + + unsupported = func() { + http.Error(w, "unsupported content-type", http.StatusBadRequest) + } + ) + + if sign == "" { + http.Error(w, "signature missing", http.StatusUnauthorized) + return + } + + if ctrl.sign.Verify(sign, 0, method, "/sink", contentType, origin, expires) { + http.Error(w, "invalid signature", http.StatusForbidden) + return + } + + if expires != "" { + if exp, err := time.Parse("2006-01-02", expires); err != nil { + http.Error(w, "could not process expiration date", http.StatusInternalServerError) + return + } else if exp.Before(time.Now()) { + http.Error(w, "signature expired", http.StatusGone) + return + } + } + + if contentType == "" { + // If content-type not explicitly set (via QS), + // try to get it from the headers + contentType = r.Header.Get("content-type") + if i := strings.Index(contentType, ";"); i > 0 { + // intentionally > 0 + contentType = contentType[0 : i-1] + } + } + + if contentType == "" { + unsupported() + return + } + + defer r.Body.Close() + + switch ctrl.svc.Process(ctx, contentType, r.Body) { + case service.ErrSinkContentProcessingFailed: + http.Error(w, "sink processing failed", http.StatusInternalServerError) + + case service.ErrSinkContentTypeUnsupported: + unsupported() + } +} diff --git a/system/system.go b/system/system.go index 372da1f35..069bcefa9 100644 --- a/system/system.go +++ b/system/system.go @@ -43,7 +43,9 @@ func Configure() *cli.Config { servicesInitialized = true // storagePath := options.EnvString("", "SYSTEM_STORAGE_PATH", "var/store") - cli.HandleError(service.Init(ctx, c.Log)) + cli.HandleError(service.Init(ctx, c.Log, service.Config{ + Corredor: *c.ScriptRunner, + })) }, @@ -127,6 +129,9 @@ func Configure() *cli.Config { func(ctx context.Context, c *cli.Config) *cobra.Command { return commands.Roles(ctx, c) }, + func(ctx context.Context, c *cli.Config) *cobra.Command { + return commands.Sink(ctx, c) + }, }, ProvisionMigrateDatabase: cli.Runners{ diff --git a/system/types/mail_message.go b/system/types/mail_message.go new file mode 100644 index 000000000..37a864809 --- /dev/null +++ b/system/types/mail_message.go @@ -0,0 +1,32 @@ +package types + +import ( + "net/mail" + "time" +) + +type ( + MailMessage struct { + Date time.Time `json:"date"` + + Header MailMessageHeader `json:"header"` + + // RawBody will be base64 encoded! + // (might contain binary data) + RawBody []byte `json:"rawBody,string"` + + // @todo parts + // Parts []... + } + + MailMessageHeader struct { + // extract common addresses + To []*mail.Address `json:"to"` + CC []*mail.Address `json:"cc"` + BCC []*mail.Address `json:"bcc"` + From []*mail.Address `json:"from"` + ReplyTo []*mail.Address `json:"replyTo"` + + Raw mail.Header `json:"raw"` + } +) diff --git a/system/types/permission_resources.go b/system/types/permission_resources.go index 951f8a61c..7e2a9f1a5 100644 --- a/system/types/permission_resources.go +++ b/system/types/permission_resources.go @@ -9,3 +9,5 @@ const ApplicationPermissionResource = permissions.Resource("system:application:" const OrganisationPermissionResource = permissions.Resource("system:organisation:") const UserPermissionResource = permissions.Resource("system:user:") const RolePermissionResource = permissions.Resource("system:role:") +const AutomationScriptPermissionResource = permissions.Resource("system:automation-script:") +const AutomationTriggerPermissionResource = permissions.Resource("system:automation-trigger:")