From 6b5ef8aaaec2d0f338566b2ea66af7c5c926ea73 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Tue, 3 Sep 2019 09:09:44 +0200 Subject: [PATCH] Basic system autuomation, mail processing --- api/system/spec.json | 383 ++++++++++++++- api/system/spec/automation_script.json | 226 +++++++++ api/system/spec/automation_trigger.json | 165 +++++++ docs/system/README.md | 223 +++++++++ pkg/automation/mail/condition.go | 198 ++++++++ .../automation/mail/condition_test.go | 40 +- system/db/mysql/static.go | 2 +- .../mysql/20190902080000.automation.up.sql | 48 ++ system/internal/service/automation_runner.go | 131 ++--- system/internal/service/automation_script.go | 4 +- system/internal/service/automation_trigger.go | 2 +- system/internal/service/service.go | 2 +- system/rest/automation_script.go | 180 +++++++ system/rest/automation_trigger.go | 166 +++++++ system/rest/handlers/automation_script.go | 185 ++++++++ system/rest/handlers/automation_trigger.go | 162 +++++++ system/rest/request/automation_script.go | 449 ++++++++++++++++++ system/rest/request/automation_trigger.go | 360 ++++++++++++++ system/rest/router.go | 3 + 19 files changed, 2807 insertions(+), 122 deletions(-) create mode 100644 api/system/spec/automation_script.json create mode 100644 api/system/spec/automation_trigger.json create mode 100644 pkg/automation/mail/condition.go rename system/internal/service/automation_runner_test.go => pkg/automation/mail/condition_test.go (62%) create mode 100644 system/db/schema/mysql/20190902080000.automation.up.sql create mode 100644 system/rest/automation_script.go create mode 100644 system/rest/automation_trigger.go create mode 100644 system/rest/handlers/automation_script.go create mode 100644 system/rest/handlers/automation_trigger.go create mode 100644 system/rest/request/automation_script.go create mode 100644 system/rest/request/automation_trigger.go 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/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/mail/condition.go b/pkg/automation/mail/condition.go new file mode 100644 index 000000000..b3b4b2696 --- /dev/null +++ b/pkg/automation/mail/condition.go @@ -0,0 +1,198 @@ +package mail + +import ( + "encoding/json" + "errors" + "net/mail" + "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" + + HMOpEqualCi HMOp = "" + 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 + // spew.Dump("doing simple string match", match, m, v) + 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 { + for i, v := range header[name] { + switch name { + case "from", + "to", + "cc", + "bcc", + "reply-to": + addr, _ := mail.ParseAddress(v) + header[name][i] = 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/system/internal/service/automation_runner_test.go b/pkg/automation/mail/condition_test.go similarity index 62% rename from system/internal/service/automation_runner_test.go rename to pkg/automation/mail/condition_test.go index fdc6c0ef4..04893449e 100644 --- a/system/internal/service/automation_runner_test.go +++ b/pkg/automation/mail/condition_test.go @@ -1,4 +1,4 @@ -package service +package mail import ( "encoding/json" @@ -11,45 +11,45 @@ func Test_makeMailHeaderChecker(t *testing.T) { tests := []struct { name string mh types.MailMessageHeader - tc TriggerCondition + tc Condition expecting bool }{ { name: "empty should not match", mh: types.MailMessageHeader{}, - tc: TriggerCondition{}, + tc: Condition{}, }, { name: "simple check", mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, - tc: TriggerCondition{Headers: []TriggerConditionHeaderMatcher{{Name: "Subject", Match: "SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "SIMPLE"}}}, expecting: true, }, { name: "simple check - no match", mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, - tc: TriggerCondition{Headers: []TriggerConditionHeaderMatcher{{Name: "Subject", Match: "complex"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "complex"}}}, expecting: false, }, { name: "simple check - no match", mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, - tc: TriggerCondition{Headers: []TriggerConditionHeaderMatcher{{Name: "From", Match: "SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameFrom, Match: "SIMPLE"}}}, expecting: false, }, { name: "simple check - name-case", mh: types.MailMessageHeader{Raw: map[string][]string{"SUBJECT": []string{"SIMPLE"}}}, - tc: TriggerCondition{Headers: []TriggerConditionHeaderMatcher{{Name: "subject", Match: "SIMPLE"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "SIMPLE"}}}, expecting: true, }, { name: "two matchers, one matches", mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, - tc: TriggerCondition{ - Headers: []TriggerConditionHeaderMatcher{ - {Name: "Subject", Match: "SIMPLE"}, - {Name: "Subject", Match: "complex"}, + tc: Condition{ + Headers: []HeaderMatcher{ + {Name: HeaderMatchNameSubject, Match: "SIMPLE"}, + {Name: HeaderMatchNameSubject, Match: "complex"}, }, }, expecting: true, @@ -57,11 +57,11 @@ func Test_makeMailHeaderChecker(t *testing.T) { { name: "two matchers, one matches, match-all=true", mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, - tc: TriggerCondition{ + tc: Condition{ MatchAll: true, - Headers: []TriggerConditionHeaderMatcher{ - {Name: "Subject", Match: "SIMPLE"}, - {Name: "Subject", Match: "complex"}, + Headers: []HeaderMatcher{ + {Name: HeaderMatchNameSubject, Match: "SIMPLE"}, + {Name: HeaderMatchNameSubject, Match: "complex"}, }, }, expecting: false, @@ -69,19 +69,23 @@ func Test_makeMailHeaderChecker(t *testing.T) { { name: "regex check", mh: types.MailMessageHeader{Raw: map[string][]string{"Subject": []string{"SIMPLE"}}}, - tc: TriggerCondition{Headers: []TriggerConditionHeaderMatcher{{Name: "Subject", Match: "^S.+$", Op: "regex"}}}, + 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: TriggerCondition{Headers: []TriggerConditionHeaderMatcher{{Name: "Subject", Match: "simple", Op: "ci"}}}, + tc: Condition{Headers: []HeaderMatcher{{Name: HeaderMatchNameSubject, Match: "simple", Op: HMOpEqualCi}}}, expecting: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - checker := (automationRunner{}).makeMailHeaderChecker(tt.mh) + 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 { 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/automation_runner.go b/system/internal/service/automation_runner.go index f08dfb923..e9d746c07 100644 --- a/system/internal/service/automation_runner.go +++ b/system/internal/service/automation_runner.go @@ -2,10 +2,6 @@ package service import ( "context" - "encoding/json" - "net/mail" - "regexp" - "strings" "time" "github.com/pkg/errors" @@ -17,7 +13,9 @@ import ( 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" ) @@ -27,10 +25,15 @@ type ( 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 @@ -41,27 +44,13 @@ type ( ApiBaseURLMessaging string ApiBaseURLCompose string } - - TriggerCondition struct { - MatchAll bool `json:"matchAll"` - Headers []TriggerConditionHeaderMatcher `json:"headers"` - } - - TriggerConditionHeaderMatcher struct { - Name string `json:"name"` - Match string `json:"match"` - Op string `json:"op"` - } -) - -const ( - AutomationResourceRecord = "compose:record" ) func AutomationRunner(opt AutomationRunnerOpt, f automationScriptsFinder, r corredor.ScriptRunnerClient) automationRunner { var svc = automationRunner{ opt: opt, + userFinder: DefaultUser, scriptFinder: f, runner: r, @@ -76,7 +65,7 @@ func (svc automationRunner) Watch(ctx context.Context) { svc.scriptFinder.Watch(ctx) } -func (svc automationRunner) OnReceivedMailMessage(ctx context.Context, mail *types.MailMessage) error { +func (svc automationRunner) OnReceiveMailMessage(ctx context.Context, mail *types.MailMessage) error { return svc.findMailScripts(mail.Header).Walk( svc.makeMailScriptRunner(ctx, mail), ) @@ -84,7 +73,12 @@ func (svc automationRunner) OnReceivedMailMessage(ctx context.Context, mail *typ // Finds all scripts that can process email func (svc automationRunner) findMailScripts(headers types.MailMessageHeader) automation.ScriptSet { - ss, _ := svc.scriptFinder.FindRunnableScripts("system:mail", "onReceived", svc.makeMailHeaderChecker(headers)). + 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 return !script.RunInUA, nil @@ -93,22 +87,24 @@ func (svc automationRunner) findMailScripts(headers types.MailMessageHeader) aut return ss } -func (svc automationRunner) RecordScriptTester(ctx context.Context, source string, mail *types.MailMessage) (err error) { - // Make record script runner and - runner := svc.makeMailScriptRunner(ctx, mail) - - return runner(&automation.Script{ - ID: 0, - Name: "test", - SourceRef: "test", - Source: source, - Async: false, - RunAs: 0, - RunInUA: false, - Timeout: 0, - Critical: true, - Enabled: false, - }) +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 @@ -194,64 +190,3 @@ func (svc automationRunner) getJWT(ctx context.Context, script *automation.Scrip return svc.jwtEncoder.Encode(intAuth.GetIdentityFromContext(ctx)) } - -func (svc automationRunner) makeMailHeaderChecker(headers types.MailMessageHeader) automation.TriggerConditionChecker { - return func(c string) bool { - var ( - err error - tc = TriggerCondition{} - re *regexp.Regexp - match bool - ) - - if err := json.Unmarshal([]byte(c), &tc); err != nil { - panic(err) // @todo replace with log - return false - } - - for _, m := range tc.Headers { - if m.Op == "regex" { - if re, err = regexp.Compile(m.Match); err != nil { - // Invalid re - continue - } - } - - for name, vv := range headers.Raw { - name = strings.ToLower(name) - if strings.ToLower(m.Name) != name { - continue - } - - for _, v := range vv { - switch name { - case "from", - "to", - "cc", - "bcc", - "reply-to": - a, _ := mail.ParseAddress(v) - v = a.Address - } - - switch m.Op { - case "regex": - match = re.MatchString(v) - case "ci": - match = strings.ToLower(v) == strings.ToLower(m.Match) - default: - match = v == m.Match - } - - if tc.MatchAll && !match { - return false - } else if !tc.MatchAll && match { - return true - } - } - } - } - - return match - } -} diff --git a/system/internal/service/automation_script.go b/system/internal/service/automation_script.go index 3d0924a3f..dde0c215e 100644 --- a/system/internal/service/automation_script.go +++ b/system/internal/service/automation_script.go @@ -50,7 +50,7 @@ func AutomationScript(sm automationScriptManager) automationScript { return svc } -func (svc automationScript) FindByID(ctx context.Context, namespaceID, scriptID uint64) (*automation.Script, error) { +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 { @@ -133,7 +133,7 @@ func (svc automationScript) Update(ctx context.Context, mod *automation.Script) return svc.scriptManager.UpdateScript(ctx, s) } -func (svc automationScript) Delete(ctx context.Context, namespaceID, scriptID uint64) (err error) { +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) { diff --git a/system/internal/service/automation_trigger.go b/system/internal/service/automation_trigger.go index 1787be0b5..eecb6ea1e 100644 --- a/system/internal/service/automation_trigger.go +++ b/system/internal/service/automation_trigger.go @@ -105,7 +105,7 @@ func (svc automationTrigger) isValid(ctx context.Context, s *automation.Script, } switch t.Event { - case "onReceived": + case "onReceive": // @todo validate default: diff --git a/system/internal/service/service.go b/system/internal/service/service.go index 7dad34e63..2ab83272a 100644 --- a/system/internal/service/service.go +++ b/system/internal/service/service.go @@ -92,7 +92,7 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { // handles script & trigger management & keeping runnables cripts in internal cache ias := automation.Service(automation.AutomationServiceConfig{ Logger: DefaultLogger, - DbTablePrefix: "system", + DbTablePrefix: "sys", DB: repository.DB(ctx), TokenMaker: func(ctx context.Context, userID uint64) (jwt string, err error) { var u *types.User 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..d0bb6051b 100644 --- a/system/rest/router.go +++ b/system/rest/router.go @@ -25,5 +25,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) }) }