From cf0d365eaf591fac17092d5bc4bdc3019a04dc68 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 12 May 2020 12:35:05 +0200 Subject: [PATCH] Fixed webhooks for local execution --- README.md | 2 +- backend/Dockerfile | 1 + backend/go-app/go.mod | 1 + backend/go-app/go.sum | 2 + backend/go-app/main.go | 169 ++++++++++++------- backend/go-app/walkoff.go | 277 ++++++++++++++------------------ backend/tests/hooks.sh | 25 ++- backend/webhook/run.sh | 4 +- frontend/src/AngularWorkflow.js | 12 +- 9 files changed, 267 insertions(+), 226 deletions(-) diff --git a/README.md b/README.md index 84d99b83..cf100ead 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Shuffle -[Shuffler](https://shuffler.io) is an automation platform for your security stack. It leverages docker for scaling and OpenAPI for integrations. It has the possibility to run across multiple isolated environments, and gives you powerful tools to track progress. +[Shuffle](https://shuffler.io) is an automation platform for your security stack. It leverages docker for scaling and OpenAPI for integrations. It has the possibility to run across multiple isolated environments, and gives you powerful tools to track progress. ## Try it Check out the [installation guide](https://github.com/frikky/shuffle/blob/master/install-guide.md) diff --git a/backend/Dockerfile b/backend/Dockerfile index 36e3c0f2..9aae88e6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,6 +7,7 @@ ADD ./go-app/main.go /app ADD ./go-app/walkoff.go /app ADD ./go-app/docker.go /app ADD ./go-app/codegen.go /app +ADD ./go-app/webhook.go /app ADD ./go-app/go.mod /app diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 8f88d119..87c07fd4 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -17,6 +17,7 @@ require ( github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.0.0 github.com/google/go-github/v28 v28.1.1 + github.com/gorilla/handlers v1.4.2 // indirect github.com/gorilla/mux v1.7.4 github.com/h2non/filetype v1.0.12 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 53d18803..92566c8b 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -125,6 +125,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/h2non/filetype v1.0.12 h1:yHCsIe0y2cvbDARtJhGBTD2ecvqMSTvlIcph9En/Zao= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index acc31784..da40013b 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -316,13 +316,14 @@ type HookAction struct { } type Hook struct { - Id string `json:"id" datastore:"id"` - Info Info `json:"info" datastore:"info"` - Actions []HookAction `json:"actions" datastore:"actions"` - Type string `json:"type" datastore:"type"` - Owner string `json:"owner" datastore:"owner"` - Status string `json:"status" datastore:"status"` - Running bool `json:"running" datastore:"running"` + Id string `json:"id" datastore:"id"` + Info Info `json:"info" datastore:"info"` + Actions []HookAction `json:"actions" datastore:"actions"` + Type string `json:"type" datastore:"type"` + Owner string `json:"owner" datastore:"owner"` + Status string `json:"status" datastore:"status"` + Workflows []string `json:"workflows" datastore:"workflows"` + Running bool `json:"running" datastore:"running"` } func createFileFromFile(ctx context.Context, bucket *storage.BucketHandle, remotePath, localPath string) error { @@ -1332,7 +1333,7 @@ func handleSettings(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("%s %s", session.Session, UserInfo.Session) + //log.Printf("%s %s", session.Session, UserInfo.Session) if session.Session != UserInfo.Session { log.Printf("Session %s is not the latest. %s", session.Username, err) resp.WriteHeader(401) @@ -1394,7 +1395,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("%s %s", session.Session, UserInfo.Session) + //log.Printf("%s %s", session.Session, UserInfo.Session) if session.Session != UserInfo.Session { log.Printf("Session %s is not the latest. %s", session.Username, err) resp.WriteHeader(401) @@ -1833,7 +1834,7 @@ func handleGetEnvironments(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("Existing environments: %s", string(newjson)) + //log.Printf("Existing environments: %s", string(newjson)) resp.WriteHeader(200) resp.Write(newjson) @@ -2708,46 +2709,88 @@ func handleNewSchedule(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": true, "message": "Created new service"}`)) } -func handleWebhookRedirect(resp http.ResponseWriter, request *http.Request) { +// Does the webhook +func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { + // 1. Get callback data + // 2. Load the configuration + // 3. Execute the workflow + path := strings.Split(request.URL.String(), "/") if len(path) < 4 { resp.WriteHeader(403) + resp.Write([]byte(`{"success": false}`)) return } - //http.Redirect(resp, request, "http://www.google.com", 301) - //https://europe-west1-shuffler.cloudfunctions.net/webhook_e843bfe2-fc36-4fa5-b682-97cdfa0c0091 + // 1. Get config with hookId + //fmt.Sprintf("%s/api/v1/hooks/%s", callbackUrl, hookId) + ctx := context.Background() + location := strings.Split(request.URL.String(), "/") - body, err := ioutil.ReadAll(request.Body) + var hookId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + hookId = location[4] + } + + // ID: webhook_ + if len(hookId) != 44 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "message": "ID not valid"}`)) + return + } + + hookId = hookId[8:len(hookId)] + + log.Printf("HookID: %s", hookId) + hook, err := getHook(ctx, hookId) if err != nil { - http.Error(resp, err.Error(), http.StatusInternalServerError) + log.Printf("Failed getting hook: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) return } - // you can reassign the body if you need to parse it as multipart - request.Body = ioutil.NopCloser(bytes.NewReader(body)) + log.Printf("HOOK FOUND: %#v", hook) + // Execute the workflow + //executeWorkflow(resp, request) - // create a new url from the raw RequestURI sent by the client - proxyScheme := "https" - url := fmt.Sprintf("%s://%s-%s.cloudfunctions.net/%s", proxyScheme, defaultLocation, gceProject, path[3]) - log.Println(url) - - proxyReq, err := http.NewRequest(request.Method, url, bytes.NewReader(body)) - - // We may want to filter some headers, otherwise we could just use a shallow copy - // proxyReq.Header = req.Header - proxyReq.Header = make(http.Header) - for h, val := range request.Header { - proxyReq.Header[h] = val - } - - httpClient := &http.Client{} - newresp, err := httpClient.Do(proxyReq) - if err != nil { - http.Error(resp, err.Error(), http.StatusBadGateway) + //resp.WriteHeader(200) + //resp.Write([]byte(`{"success": true}`)) + if hook.Status == "stopped" { + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The webhook isn't running. Click start to start it"}`))) return } - defer newresp.Body.Close() + + if len(hook.Workflows) == 0 { + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflows are defined"}`))) + return + } + + for _, item := range hook.Workflows { + log.Printf("Running for workflow: %s", item) + workflow := Workflow{ + ID: "", + } + + workflowExecution, executionResp, err := handleExecution(item, workflow, request) + + if err == nil { + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) + return + } + + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp))) + } } // Starts a new webhook @@ -2780,6 +2823,9 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": false}`)) return } + + log.Println("Data: %s", string(body)) + ctx := context.Background() var requestdata requestData err = yaml.Unmarshal([]byte(body), &requestdata) @@ -2827,7 +2873,8 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { } hook := Hook{ - Id: newId, + Id: newId, + Workflows: []string{requestdata.Workflow}, Info: Info{ Name: requestdata.Name, Description: requestdata.Description, @@ -2847,6 +2894,9 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Running: false, } + log.Printf("Hello") + + // FIXME: Add cloud function execution? //b, err := json.Marshal(hook) //if err != nil { // log.Printf("Failed marshalling: %s", err) @@ -2855,21 +2905,21 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { // return //} - environmentVariables := map[string]string{ - "FUNCTION_APIKEY": user.ApiKey, - "CALLBACKURL": "https://shuffler.io", - "HOOKID": hook.Id, - } + //environmentVariables := map[string]string{ + // "FUNCTION_APIKEY": user.ApiKey, + // "CALLBACKURL": "https://shuffler.io", + // "HOOKID": hook.Id, + //} - applocation := fmt.Sprintf("gs://%s/triggers/webhook.zip", bucketName) - hookname := fmt.Sprintf("webhook_%s", hook.Id) - err = deployWebhookFunction(ctx, hookname, defaultLocation, applocation, environmentVariables) - if err != nil { - log.Printf("Error deploying hook: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Issue with starting hook. Please wait a second and try again"}`))) - return - } + //applocation := fmt.Sprintf("gs://%s/triggers/webhook.zip", bucketName) + //hookname := fmt.Sprintf("webhook_%s", hook.Id) + //err = deployWebhookFunction(ctx, hookname, defaultLocation, applocation, environmentVariables) + //if err != nil { + // log.Printf("Error deploying hook: %s", err) + // resp.WriteHeader(401) + // resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Issue with starting hook. Please wait a second and try again"}`))) + // return + //} hook.Status = "running" hook.Running = true @@ -5465,6 +5515,7 @@ func init() { var err error ctx := context.Background() + log.Printf("Running INIT process") dbclient, err = datastore.NewClient(ctx, gceProject) if err != nil { log.Fatalf("DBclient error during init: %s", err) @@ -5483,11 +5534,11 @@ func init() { } } - r := mux.NewRouter() - r.HandleFunc("_ah/health", healthCheckHandler) + log.Printf("Finished INIT") + + r := mux.NewRouter() + r.HandleFunc("/api/v1/_ah/health", healthCheckHandler) - // Webhook redirect to the correct cloud function - r.HandleFunc("/functions/webhooks/{key}", handleWebhookRedirect).Methods("POST", "OPTIONS") // Sends an email if the right things are specified r.HandleFunc("/functions/sendmail", handleSendalert).Methods("POST", "OPTIONS") r.HandleFunc("/functions/outlook/register", handleNewOutlookRegister).Methods("GET", "OPTIONS") @@ -5552,11 +5603,17 @@ func init() { r.HandleFunc("/api/v1/workflows/{key}", getSpecificWorkflow).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}", saveWorkflow).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}", deleteWorkflow).Methods("DELETE", "OPTIONS") + + // Triggers + // Webhook redirect to the correct cloud function r.HandleFunc("/api/v1/hooks/new", handleNewHook).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/hooks/{key}", handleWebhookCallback).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/hooks/{key}/delete", handleDeleteHook).Methods("DELETE", "OPTIONS") + + // Trigger hmm r.HandleFunc("/api/v1/triggers/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") - // Weird API's for random things + // OpenAPI configuration r.HandleFunc("/api/v1/verify_swagger", verifySwagger).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/verify_openapi", verifySwagger).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/get_openapi_uri", echoOpenapiData).Methods("POST", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 11b0aa8f..2e710d88 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -167,21 +167,23 @@ type Action struct { // Added environment for location to execute type Trigger struct { - AppName string `json:"app_name" datastore:"app_name"` - Status string `json:"status" datastore:"status"` - AppVersion string `json:"app_version" datastore:"app_version"` - Errors []string `json:"errors" datastore:"errors"` - ID string `json:"id" datastore:"id"` - IsValid bool `json:"is_valid" datastore:"is_valid"` - IsStartNode bool `json:"isStartNode" datastore:"isStartNode"` - Label string `json:"label" datastore:"label"` - SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"` - LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false` - Environment string `json:"environment" datastore:"environment"` - TriggerType string `json:"trigger_type" datastore:"trigger_type"` - Name string `json:"name" datastore:"name"` - Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` - Position struct { + AppName string `json:"app_name" datastore:"app_name"` + Description string `json:"description" datastore:"description"` + LongDescription string `json:"long_description" datastore:"long_description"` + Status string `json:"status" datastore:"status"` + AppVersion string `json:"app_version" datastore:"app_version"` + Errors []string `json:"errors" datastore:"errors"` + ID string `json:"id" datastore:"id"` + IsValid bool `json:"is_valid" datastore:"is_valid"` + IsStartNode bool `json:"isStartNode" datastore:"isStartNode"` + Label string `json:"label" datastore:"label"` + SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"` + LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false` + Environment string `json:"environment" datastore:"environment"` + TriggerType string `json:"trigger_type" datastore:"trigger_type"` + Name string `json:"name" datastore:"name"` + Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` + Position struct { X float64 `json:"x" datastore:"x"` Y float64 `json:"y" datastore:"y"` } `json:"position"` @@ -1546,74 +1548,17 @@ func cleanupExecutions(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte("OK")) } -func executeWorkflow(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in execute workflow: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - location := strings.Split(request.URL.String(), "/") - - var fileId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return +func handleExecution(id string, workflow Workflow, request *http.Request) (WorkflowExecution, string, error) { + ctx := context.Background() + if workflow.ID == "" || workflow.ID != id { + log.Printf("UPDATING WORKFLOW") + tmpworkflow, err := getWorkflow(ctx, id) + if err != nil { + log.Printf("Failed getting the workflow locally: %s", err) + return WorkflowExecution{}, "Failed getting workflow", err } - fileId = location[4] - } - - if len(fileId) != 36 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Workflow ID to execute is not valid"}`)) - return - } - - //memcacheName := fmt.Sprintf("%s_%s", user.Username, fileId) - var workflow Workflow - ctx := context.Background() - //if item, err := memcache.Get(ctx, memcacheName); err == memcache.ErrCacheMiss { - // // Not in cache - // log.Printf("Workflow %s not in cache.", memcacheName) - tmpworkflow, err := getWorkflow(ctx, fileId) - if err != nil { - log.Printf("Failed getting the workflow locally: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflow = *tmpworkflow - //} else if err != nil { - // log.Printf("Error getting item: %v", err) - //} else { - // // FIXME - verify if value is ok? Can unmarshal etc. - // log.Printf("Got workflow %s from cache", fileId) - // err = json.Unmarshal(item.Value, &workflow) - // if err != nil { - // log.Printf("Failed cache unmarshal in executeworkflow for %s", fileId) - // resp.WriteHeader(401) - // resp.Write([]byte(`{"success": false}`)) - // } - //} - - // FIXME - have a check for org etc too.. - // FIXME - admin check like this? idk - if user.Id != workflow.Owner && user.Role != "admin" && user.Role != "scheduler" && user.Role != fmt.Sprintf("workflow_%s", fileId) { - log.Printf("Wrong user (%s) for workflow %s (execute)", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return + workflow = *tmpworkflow } if len(workflow.Actions) == 0 { @@ -1631,17 +1576,13 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { if !workflow.IsValid { log.Printf("Stopped execution as workflow %s is not valid.", workflow.ID) - resp.WriteHeader(403) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "workflow %s is invalid"}`, workflow.ID))) - return + return WorkflowExecution{}, fmt.Sprintf(`workflow %s is invalid`, workflow.ID), errors.New("Failed getting workflow") } workflowBytes, err := json.Marshal(workflow) if err != nil { log.Printf("Failed workflow unmarshal in execution: %s", err) - resp.WriteHeader(http.StatusInternalServerError) - resp.Write([]byte(`{"success": false}`)) - return + return WorkflowExecution{}, "", err } //log.Println(workflow) @@ -1649,29 +1590,22 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { err = json.Unmarshal(workflowBytes, &workflowExecution.Workflow) if err != nil { log.Printf("Failed execution unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return + return WorkflowExecution{}, "Failed unmarshal during execution", err } makeNew := true if request.Method == "POST" { - body, err := ioutil.ReadAll(request.Body) if err != nil { - log.Printf("Failed hook unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return + log.Printf("Failed request POST read: %s", err) + return WorkflowExecution{}, "Failed getting body", err } var execution ExecutionRequest err = json.Unmarshal(body, &execution) if err != nil { log.Printf("Failed execution POST unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return + return WorkflowExecution{}, "", err } // FIXME - this should have "execution_argument" from executeWorkflow frontend @@ -1706,16 +1640,12 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { oldExecution, err := getWorkflowExecution(ctx, referenceId[0]) if err != nil { log.Printf("Failed getting execution (execution) %s: %s", referenceId[0], err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist."}`, referenceId[0]))) - return + return WorkflowExecution{}, fmt.Sprintf("Failed getting execution ID %s because it doesn't exist.", referenceId[0]), err } - if oldExecution.Workflow.ID != fileId { + if oldExecution.Workflow.ID != id { log.Println("Wrong workflowid!") - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad ID %s"}`, referenceId))) - return + return WorkflowExecution{}, fmt.Sprintf("Bad ID %s", referenceId), errors.New("Bad ID") } newResults := []ActionResult{} @@ -1745,14 +1675,10 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { err = setWorkflowExecution(ctx, *oldExecution) if err != nil { log.Printf("Error saving workflow execution actionresult setting: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed setting workflowexecution actionresult in execution: %s"}`, err))) - return + return WorkflowExecution{}, fmt.Sprintf("Failed setting workflowexecution actionresult in execution: %s", err), err } - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Updating %s with your information."}`, referenceId[0]))) - return + return WorkflowExecution{}, "", nil } } @@ -1762,9 +1688,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { oldExecution, err := getWorkflowExecution(ctx, referenceId[0]) if err != nil { log.Printf("Failed getting execution (execution) %s: %s", referenceId[0], err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist."}`, referenceId[0]))) - return + return WorkflowExecution{}, fmt.Sprintf("Failed getting execution ID %s because it doesn't exist.", referenceId[0]), err } workflowExecution = *oldExecution @@ -1790,9 +1714,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { // FIXME - regex uuid, and check if already exists? if len(workflowExecution.ExecutionId) != 36 { log.Printf("Invalid uuid: %s", workflowExecution.ExecutionId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Invalid uuid."}`)) - return + return WorkflowExecution{}, "Invalid uuid", err } // FIXME - find owner of workflow @@ -1841,9 +1763,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { //log.Println(action.Environment) if action.Environment == "" { - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Environment is not defined for %s"}`, action.Name))) - return + return WorkflowExecution{}, fmt.Sprintf("Environment is not defined for %s", action.Name), errors.New("Environment not defined!") } newActions = append(newActions, action) } @@ -1875,9 +1795,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { err = setWorkflowExecution(ctx, workflowExecution) if err != nil { log.Printf("Error saving workflow execution for updates %s: %s", topic, err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed setting workflowexecution"}`))) - return + return WorkflowExecution{}, "Failed getting workflowexecution", err } log.Printf("Environments: %#v", environments) @@ -1914,32 +1832,71 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) { } } - //body, err := json.Marshal(workflow) - //if err != nil { - // log.Printf("Failed workflow SET marshalling: %s", err) - // resp.WriteHeader(http.StatusInternalServerError) - // resp.Write([]byte(`{"success": false}`)) - // return - //} + return workflowExecution, "", nil +} - //item := &memcache.Item{ - // Key: memcacheName, - // Value: body, - // Expiration: time.Minute * 10, - //} - //if err := memcache.Add(ctx, item); err == memcache.ErrNotStored { - // if err := memcache.Set(ctx, item); err != nil { - // log.Printf("Error setting item: %v", err) - // } - //} else if err != nil { - // log.Printf("error adding item: %v", err) - //} else { - // log.Printf("Set cache for %s", item.Key) - //} +func executeWorkflow(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) - return + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in execute workflow: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + location := strings.Split(request.URL.String(), "/") + + var fileId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + fileId = location[4] + } + + if len(fileId) != 36 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Workflow ID to execute is not valid"}`)) + return + } + + //memcacheName := fmt.Sprintf("%s_%s", user.Username, fileId) + ctx := context.Background() + workflow, err := getWorkflow(ctx, fileId) + if err != nil { + log.Printf("Failed getting the workflow locally: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // FIXME - have a check for org etc too.. + // FIXME - admin check like this? idk + if user.Id != workflow.Owner && user.Role != "admin" && user.Role != "scheduler" && user.Role != fmt.Sprintf("workflow_%s", fileId) { + log.Printf("Wrong user (%s) for workflow %s (execute)", user.Username, workflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + workflowExecution, executionResp, err := handleExecution(fileId, *workflow, request) + + if err == nil { + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) + return + } + + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp))) } func stopSchedule(resp http.ResponseWriter, request *http.Request) { @@ -3434,21 +3391,21 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { } // This is here to force stop and remove the old webhook - image := "webhook" - err = removeWebhookFunction(ctx, fileId) - if err != nil { - log.Printf("Function removal issue for %s-%s: %s", image, fileId, err) - if strings.Contains(err.Error(), "does not exist") { - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true, "reason": "Stopped webhook"}`)) + //image := "webhook" + //err = removeWebhookFunction(ctx, fileId) + //if err != nil { + // log.Printf("Function removal issue for %s-%s: %s", image, fileId, err) + // if strings.Contains(err.Error(), "does not exist") { + // resp.WriteHeader(200) + // resp.Write([]byte(`{"success": true, "reason": "Stopped webhook"}`)) - } else { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Couldn't stop webhook, please try again later"}`)) - } + // } else { + // resp.WriteHeader(401) + // resp.Write([]byte(`{"success": false, "reason": "Couldn't stop webhook, please try again later"}`)) + // } - return - } + // return + //} log.Printf("Successfully deleted webhook %s", fileId) resp.WriteHeader(200) diff --git a/backend/tests/hooks.sh b/backend/tests/hooks.sh index 7905b184..3a82ff61 100644 --- a/backend/tests/hooks.sh +++ b/backend/tests/hooks.sh @@ -1,7 +1,7 @@ #curl localhost:5000/api/v1/hooks # Starts a webhook -# curl localhost:5000/api/v1/hooks/d6ef8912e8bd37776e654cbc14c2629c/start +#curl localhost:5000/api/v1/hooks/d6ef8912e8bd37776e654cbc14c2629c/start # Runs a request towards the webhook created #curl -XPOST localhost:5002/webhook -d '{"helo": "hi"}' @@ -14,11 +14,28 @@ #curl -X POST "https://europe-west1-shuffle-241517.cloudfunctions.net/webhook_982995716e67c3a549092d3a3a7921cd" -H "Content-Type:application/json" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data '{"name":"Keyboard Cat"}' -v +## GET HOOK +#curl http://localhost:5001/api/v1/hooks/b4ba07c9-45d4-41f2-b260-83c8e99eba0c -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" -#jcurl http://localhost:5001/api/v1/hooks/b4ba07c9-45d4-41f2-b260-83c8e99eba0c -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" #curl https://shuffler.io/api/v1/hooks/b4ba07c9-45d4-41f2-b260-83c8e99eba0c -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" -#curl -X POST "http://localhost:8080" -H "Content-Type:application/json" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data '{"test": {"hello": "HEYOOOO"}}' -v + #curl -X POST "https://europe-west1-shuffle-241517.cloudfunctions.net/webhook_3ceff795-ce9a-43a2-a2f5-d4401a6e772d" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data 'wut' -curl POST "https://europe-west1-shuffler.cloudfunctions.net/outlooktrigger_be4dbb0a-d396-4544-bc36-e57d1bdb2e40" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data 'wut' -vvv +#curl POST "https://europe-west1-shuffler.cloudfunctions.net/outlooktrigger_be4dbb0a-d396-4544-bc36-e57d1bdb2e40" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data 'wut' -vvv +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5001/api/v1/hooks/webhook_9a4efb03-089c-48b7-82f3-f76500a3ebc0" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v diff --git a/backend/webhook/run.sh b/backend/webhook/run.sh index e45a9c6c..c9914de7 100644 --- a/backend/webhook/run.sh +++ b/backend/webhook/run.sh @@ -6,8 +6,8 @@ docker build . -t webhook docker run -d \ -e "HOOKPORT=5001" \ -e "URIPATH=/webhook" \ - -e "CALLBACKURL=http://192.168.159.151:5000/api/v1/hooks/d6ef8912e8bd37776e654cbc14c2629c/result" \ - -p 5001:5001 \ + -e "CALLBACKURL=http://192.168.3.6:5001/api/v1/hooks/d6ef8912e8bd37776e654cbc14c2629c/result" \ + -p 6000:6000 \ --name webhook \ -h webhook \ --restart always \ diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index e10e468e..59e7c251 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -78,10 +78,12 @@ function useWindowSize() { const splitter = "|~|" //const referenceUrl = "https://shuffler.io/functions/webhooks/" -const referenceUrl = window.location.origin+"/functions/webhooks/" +//const referenceUrl = window.location.origin+"/api/v1/hooks/" + console.log(window.location) const AngularWorkflow = (props) => { - const { globalUrl, isLoggedIn, isLoaded } = props; + const { globalUrl, isLoggedIn, isLoaded } = props; + const referenceUrl = globalUrl+"/api/v1/hooks/" const alert = useAlert() const [bodyWidth, bodyHeight] = useWindowSize(); @@ -1515,13 +1517,14 @@ const AngularWorkflow = (props) => { "name": "Webhook", "type": "TRIGGER", "status": "uninitialized", - "description": "Realtime HTTP trigger", "trigger_type": "WEBHOOK", "errors": null, "large_image": 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QA/wD/AP+gvaeTAAAAB3RJTUUH4wYNAxEP4A5uKQAAGipJREFUeNrtXHt4lNWZf8853zf3SSZDEgIJJtxCEnLRLSkXhSKgTcEL6yLK1hZWWylVbO1q7SKsSu3TsvVZqF2g4haoT2m9PIU+gJVHtFa5NQRD5FICIUAumBAmc81cvss5Z/845MtkAskEDJRu3r8Y8n3nfc/vvOe9zyDOOQxScoRvtAA3Ew2C1Q8aBKsfNAhWP2gQrH7QIFj9oEGw+kGDYPWDBsHqBw2C1Q+SbrQAPSg+/ULoRkvTjf4uwOKMAeeAEMI4AaBuf7rRhG5kIs05Zxxh1AUQ5yymUkVFgLBFxhZzbw///wGLUyZ2zikLn2oIVJ3o+NtZ5Xyb5u/gmgYAyCTLLqdlRKajaFRqeZFtTA7C+BJk5MZo2Y0Ai3EOHGGshyIX393btnNv5FQjjSoIYyQRRDBgdOkxyriuc8aJzeIozMu4d2rG16YQm4UzhtANULHrDRZnDGHMGW/b9lHzxh3RxlZslrHFjDAG4JxziBcHAUIIAHHGWFRhqmYblZ3z7bmZc24HAM75dTZk1xUsThkiWPn84umVv/btrSF2K7aYOGPA+pYBYQQIs5hCo8qQGRNGP/9vpky3WPAfECyxseCntSef+6Xq8UupDk4Z9Jc7QohgzR+yDMsY9/OlzpIx1xOv6wSW2JJvb03tM78AxrHVzHWaiAJGAMA5F4p2KYzgnHMG3WVEEqGRGDbJhWt+kFpedN3wuh5gCTsVPHzyb9/9L845Nkmcsm5CEMw0yiIxzhg2ycgkAQemqFyniBBiMyOJXOYVRcNmuXjDMntBnmBx84PFOSCktvmOLHxR9fiJ1Ry/bYQxZ0wPhk3pLtek4tQJhda84cRpA8Y0bzBS3xz4tDZYXav5QlKKXTwcjxcNxyy3ZJVufkFKtQtG/whg1f77Lzy7K+U0Z/ztQwTTiIJN0rAFdw97+G5TRlrihjkAAuVzT8tbu1ve3s01SmzdsZaI5g1m/cudY158/Doo18CCJTbg2V158plfXLLocUjpoYhtdE7+T5bYx+WKaJNR2mW8GOMciERESBWuPXdq2brIuRbJYU3QTRqOFq37oWtSyUDjNZBHwQFhzCk9v3knkqV4Iy2QcpaMKfnf5fZxuZxSRhlgREwykSVMCCaEyLJkkhHGlFKuU3tBXvH/LncU5Okd0QRzzjk/v2kngAjKBpAGULPEOXv/8umJ7/+3lGLvUgeMuKKZhrpLNv6nKcPFKeUIYYxVVa2srKyurm5ra+Ocp6enl5WVTZo0yW63M8YQ40giyueeo4+u1HwhJEtG2IEwohFl/Gv/kTqhcECVa8CrDhd3HUhw/MCBUzbquYXxSFVVVb322mv19fWcc0IIAFBKt2/fnp2dvWjRorvuuosBA52ah6ePfPYbtc++KpnkrmNGiGm65739qRMKYSAt8ICBxTnCWPd3hGrqsNXEO2N0RLAeDKffNTHtjjLOmEBq+/bta9askSQpJSUFRKjVeafa29tXrlx57ty5b3/72wwYMDZkZrl76q3eTw5LDptwjpxxYjEFDp2gkRixWQbOLQ6UxooNh+sa1Yu++CvDOUeEDJ03AwAYYxjjysrKNWvW2Gw2i8VCKaWUMsYYY+LfJpPJ7Xa/8cYbW7duxRgzygAga96M7k6TI5OstHgi9ecN1jcTWOI6hOuamKp12V2EWEyz5g1LuTUfAIgkqaq6YcMGSZIwxoyxnssI4FJSUjZv3nz+/HkiSxwg5bYCS3YGUzUDMoQRjamR+maD9U0FFgAAKOfb4j8ijLiq2gvzsNlENR0A/vrXv545c8ZqtV4WqUuwcy5JUiAQePfddwGA6TpxWO1jb2GKGuf+EHDeye6m0ywEAKB6At19E+KM20ZmCwwA4NChQ8ncGsaY2WyuqakBAIwwAFhGZHLGoOsuckBIC3R08b6ZwAIEACyqAEJxJ80BITnNCSJPBmhpaSGEJIMXIcTr9YZCIRFkSSkO4Im4cI32uc7fJ1gCMdTjUvD4/I5Smnwk2R3TnvhybJYHdDcDBxYHAOKwivwu/r/VNp+xc5fL1Yu1iidKqdvtdjgcwBgA6MEIksilAjQAAAIO8pDUK+D4dw4WBwAwZ6bF6xFwQBIJ1zaI3QFAfn5+Msol4vuioiKEEKUMAMKnGvVAB4sqAIAIRhghgq25WWAsfTOBBQAA1lHZ8QER54xYzKEjdbHzF4kkAcC0adNSU1P7xIsxJsvytGnToNPYDX/kazmP3WcdORwY1/whzRfEZpM9PxcGMkMcqAheSOwoHNmtSMAByUT1Bi5s+yj3yflU1YYPH37fffdt3rw5IyND07TLLoUxjkQixcXFZWVlAIAJBoC020vTbi/llEbPtgQ/O+X75DAgZM0bBgBxd/MmAUtIbBuTYxuT03H8LLaZRbGYMyY5bK1v7U6/a5J93C3A2KJFi86ePfvxxx+73W6EEO8kYyURZ/l8Pr/f73K5OOcIIc4YcECECBZZ/zIjsU49AERefPHFAVpatFH1QNi3t4ZYLV1FAkJoROk4Vp9RMRmbTRjgjqlTFUU5fvx4OBxmjBFCJEmKx0uW5ZaWFoTQhAkTRJKEuspeHBgDQNehDD+AYCEEgJBleMbFXQeYonRFp5xjsxxrbus4cTZ9ZjkyyRjQpMmTJk2a5HA4CCHBYDAYDJrNXb17zrnZbD516tTkyZPdbrdQrk4uCGE80JWsAQdLtOYlp412RPz7jxK7pas/yDmxWiKnmwNVf3NNKpZTHUyn6RkZEyZMqKiomDlzJqX02LFjstwVNxFCOjo6gsHgV77ylXiwricNJFidymUvHOn96FPNF8Iy6YqBOCdWS6y5zbPrgGVYhn3sCM454xwB2Gy2iRMnyrJ84MABi8Ui7iPn3GKxnD59urCwMCcnh4kO/j8SWIAQZ4xYTObsjIvv7sMmU7euKufEYqJR5eJ7+yOnmx35t5jcKUh08TkvLS2tra09d+6c2Ww2Klyapn3++ecVFRX4RkwgDXyvDWPOmHvabTmL7lG9ASSR+L9yypAkSU57+wdVNQ8tC59sAIRwZ1S5cOFCWe6qiDLG7Hb70aNH33vvPQCgdMDd3/UGS+AFnOc+9VBGxRTN40+skXMOCBBBriml9nG5wAEwEuWtwsLCmTNnhkIhUWgWeFkslt///vfBYDDJDPwmA6sTMzT25e+4Z5TTmJI43kcZtphzn3jwEnaXHkcAsGDBApfLpeu6+CjcYlNT0zvvvCOwS2DSM0z7uwNLVIF7ExEhTimxmrMeuJPTbrYZEaIHw8Mevts2dgSnzIi/EUKU0pycnHvuuaejo8MwUowxh8Oxffv2pqYmQohRiTbsmiDOuZAqyUR9wMHinAuMMMaEkN7dEyKEa3rz5h0ovm6DEI0ptlHZ2YvuATFXFC8cxgDw4IMPZmdnK4piKJckScFg8Le//a1AhxAiwlRKaSwWi8ViQhOFVBhjQ85rBOvq0x3hvAkhuq7X1tZ++OGHxcXFM2fOFBF2IqyUIYJb3v4gWH1STnMa2SLCiCvaiMUPSE5bz2EYsf/U1NSHHnpo9erVZrNZGHVKqcPh+Oijj2bNmjV06NCqqqqzZ8+2trYGAgFFUcRVTU1NzcrKys/PLykpycvLEwbusrIlT1fTZBVGAWOsKMr777+/Y8eOc+fOeb3emTNnrlq16jICcQ4IKa3tRx75T70jiiQiDJPoS6fdXlr0Pz/svX+l6/rSpUtPnz4dX60XKqZpWjgcRgiJrodgLVRJ3EGbzTZ69OhZs2bNmjXL4XCI168Osn6DZSB18ODB1157ra6uzmw2WywW4dc3bNhg5LpdrzCGMD790uutf/hIdjl5nMvnjJX8eoWjaOSlSeTLkUB///79K1asEN3pS6IjZGi3IVjXxhASMlBKFUVRVTU7O3vBggWzZ88mhFydivXvBYECY2z9+vXPPfdcY2Ojy+Uym82Ct8fjOXnyJHSv/wqkAlV/a9uxV0qxG0hdsuvzZjqKRl6aXL6SiBhzzqdMmTJ58uR4S28cSbyNN8joPAKA1Wp1uVxer/fnP//5s88+29zcjDG+ijCtH2AJ4SKRyIoVK7Zs2eJwOERb1HBDlFLRgOl2whhzxhrXvgPxCQpCLKZYc4flPHYf9LDrl2UNAN/85jeFCvd3kwI4WZbdbndNTc3SpUurqqqEJx0QsARSsVhs+fLl+/btGzJkiGh/xj9gsVg+++wzADBiSGHIW9/5MFBdSzq77QIdqqgjFv+z5HJyyvrstosYNT8/v6KioqOjw1i/60jifJ+4gD0dNOdc13Wn0xmLxZ5//vmPP/64v3j17xquWrWqqqoqLS1N1/WEzXDOI5GI1+v1+/1CMuAcEay2+Zp/vZ3YrF1ICbs+pSzz3qnimWRYGzGqqKkaKAhQdF0PhUJ+vz8cDiuKEovFxEdVVRMgEyomy/JPfvKTQ4cOCfuV5PaTMvDCJG3ZsuVXv/qV2+1OQIoQEo1GAWDOnDmPPPJIenr6pWImZYjg+pc3try9W3aldLPrlJX8erlj/Khe7HpPopQSQt56661169aJthBCKBqNapqWlZVVXFxcVFSUk5PjcDgope3t7bW1tZWVlWfPnrVarbIsx4MiOiB2u/2Xv/zl8OHDk6z59A2WQOrUqVPf+973hJLHvyJqdaNGjXr66adLSkqEQgHnopETrD55bPFPsVmOL5NqvmD2wntGPvP1/k4Ziy0pivLEE080NzcDgKIo48ePv/fee6dMmeJ0Onu+oqrqn//8502bNnk8HgFivOShUKi8vHzVqlVJgtW3rGKVTZs2xWKxhNwVYxwIBO666661a9eWlJRQTeeMI4wRJqK60LD2HR7fuUGIKYr1lqycbyVl13tKIvr43/jGN/x+//Dhw1944YVXX331q1/9ak+kdF3XNE2W5YqKivXr1992220i9zYeoJQ6nc7Kyspdu3aJlfsWoHfNEmpVXV397LPP2my2BE0OBoMPP/zwkiVLOOeMUiJJwHnH8TO+/UeiDa1Ki6fj+JluI3oEa/6O/Je/k3nftGsZXldVdffu3VOnTk1JSaGUCn1vbW09d+5cOBx2OBy5ublZWVnQOYQjYteXXnpp37594hUDfVVVc3Jy1q9fbzKZ+tSvpNKdnTt3JgBPCAkEAnPnzl2yZAljDBgnktRx4lzDq28Gqk4wRUUYIUnCVnPcMCPWO6JpU0oy75uWvF2/LMmyPGfOHM650J36+vrNmzfX1NSIfgfG2OFwFBUVPfzww7feeit0th2XL1/+gx/8oK6uzkgDhAc/c+bM/v37p0+f3idYvUksIvW2trbDhw8b5V2hU+FwuLS0dOmTS4WRwhK5+O7eo4te8h84hq0mOS1FSnUQmxl6qO2wf60A0ZK5NtJ1Xdd1WZb37Nnz5JNP7tmzR6QQTqfTbrfrun7w4MGnn3769ddfN3Jsi8Xy/e9/32QyJUQ8CKEPP/wwGaa9gSUWPXLkiNfrja9YiqTs8ccfl2SJ6RQT4v3o01PPr0cESyl2YJxTyinrhghCTNflNKejIA/6b60SSKQ4Aqkf//jHACDmK1knIYQcDofD4di0adPatWtF2EUp7RmpCeWqra31er0iALpKsAQdO3as2wsYh8Ph8vLykpISRimRJc0XPLPqDWySESH8ijEeRwhxnTJNv/yfe6WEhzVNa2xsXLt27cqVKyVJkiSpZ2wpUov09PS33nrrk08+MZz47Nmz7Xa78bw4eK/XW1dXB32NWPZms0QW1tDQEN/yFI7jjjvuABGgE3Lhjx/Hmi/IQ1J76wlzQBLR/aHQkTpLdgZw0HTtpZdeunDhgqGz8ZoLcSMLRj3PCFxCodCFCxcikYhwgldyZAJok8n05ptvTps2Texi9OjRY8eOPXbsmOGvEEK6rp85c2bixIlXCZYR1Hi93viIgVJqs9ny8/MBQMQH/n2fYbOczHcGOQf/viMZX5sCCBBAQ0NDY2OjyMMNXC4rSYJUGGNZlsVESe8cRc3+9OnTtbW1BQUFlFJJkvLz82tqarpVaxFqaWnpU/4+vGEsFotGo0aiLyyl3W53uVwAgDGm4ZjS6kXdu+1XQh+bpGhDi1iIcS5JktVqFT4brnwFeiIoVCbJtE7U3c6ePVtQUCBOJTs7O1EwjEWWdk2hA6XUaBYYS4uzvfSRMZ5kbsUBEKLRGNN0LEuqoookySif94JyUuv3SqFQyPh3zwhWdCT7BKsPA08Iib+D4hCi0WhHR4f4KDltl8rEffo3BMA5sVmQLAFAIBgIh8M9HRBOmvrVkbbZbMa/e842iX31uUgfmmWxWGw2WyAQiIcvHA6fP38+JyeH6TqRZcf40aGj9cRm4dDbvUAIMVW3jc4RW2xtbY1EIglZAQBEo9FkWvPC5ffp7MWTsizn5nbNuXk8noS3OOd2ux3iCor9A0v4HbPZ7Ha7m5ubDdcrzNaRI0cmTpwoBhIzKiZf+MOfk/i2M0IYDZn5ZfHh5MmT8ZUWQ+hx48bFB8BXIoxxXV2dqMD08rDwUSNGjCgoKIDOQlt9fX38W8K/Z2RkwLWEDmJUatSoUdXV1cauGGMmk+ngwYOLFi2SZZkzlvJP49Lvnti2Y4+c7uJXCKOQLGntAff0L6XdUSbKMtXV1fGBrrAamZmZr7zyitVq7f2ERU6za9eun/70p737REJIJBKZO3euLMuiwhMMBk+cOGHMTxjcher1cUJ9PlFaWhpflhH6X19ff+DAAQBgjAPAyB9+016Qp3mDSJa6RecIxE9baN6AbXTOmBWPcgCEUV1d3fHjx+NrxMJnFRUVWa1WsfleYlShCxUVFXPnzvV4PKKv01OnJElqb2+/884777//fuP/9+3b19raGn9OIhgaM2YMXIuBFxKUlZVlZmYmXBlRC1RVlUiEMyanOYvW/tBVXqRe9NGoIhwfIMQp18NRzRtMu71s/Gv/Ycp0i2+hbNu2LRqNxhdMBARixBbiAtFeiHP+1FNPPfDAA+3t7bFYTJRMBWGMNU3zeDzTp09ftmyZWJ8QEovFtm7dmqDRqqrm5uaOHDkS+mqR9TZyJA7QarU2NzcfO3ZM3A7oHDg4f/68pmnl5eWMMQQgOW0Zs6eYh6Vr7UE9GGaKyhmT7NaU0rF5Tz2Uu/QhyWnTNU2S5YMHD77++uvxpt0olSxevFiSJKOL1QsZKnb77bePGDGiqanJ4/FEIhHRkWaMDRs27Fvf+tbixYvFRJy4uZs2bfrLX/5idA+h01/df//9ZWVlotrTC9Ok6lmnT59eunRpwkIY446OjieeeGLevHmMMc4YxgRhxClTWjxaewAINg8dYspwAQCjjDEqyXJTU9Mzzzzj9/vjj5cQ4vf7n3zyyfnz5wvL0jtSRtdPhKaiXFVbW1tfX9/R0WGz2UaOHFlYWGhcc1HS2r1796pVq4wjN0CXJGn9+vXJFJeTLSuvXr1627ZtLpcrvnIGAOFweMGCBY899pjwL1TTESGks1bFAZiuIwBECELoxIkTK1eu9Hg88dbKMO3r1693OBy9SywagoSQ999/v7m5+ZFHHjFKLglnKXA0ksrdu3e/8sorwrolHNK8efOWLl2aTNu1b7CE9F6v97vf/a7P54uvBwlRgsFgUVHRwoULy8vLeyqFeP3ixYtbt27dtm2bqAvHx1aijvjCCy/MmDGjd4kNULZs2bJx40Zd10ePHj1//vzp06dbLBYDSsFRHJ5o3/3mN795++23E+IykT87nc5169YZTZZrBctQrn379okGekLZRLhnSunYsWPLy8sLCgoyMzOtVquu636/v6Gh4bPPPqupqfH5fA6HI+FLmGLAfc6cOc8991zvSInNqKq6Zs2anTt3ulwukUsoipKXlzdt2rSJEyfm5eWJ2FLI3NLScuDAgR07djQ0NIgUJ0HsQCCwbNmyu+++O8lufrKzDmK53/3ud+vWrXO73QkJneAUi8UURcEYm81mSZIYY6qqappGCLFarT2rTuIrl+PHj+8zthJ/8vl8K1eurK6uNqyBuGLCqJvN5vT09IyMDNHF8Xq9LS0twWDQYrGIznkC6/b29vnz5yd5AfsHloHXhg0b3njjjbS0tJ5lOSF6fMXOqED1fFiSJL/fn5+fv2rVqoTR9ssiFYlEHn/88aampiFDhqiq2pMvY0zTNF3XjWkRWZbFmfVk3d7ePmvWrBUrViTjeQ3qx7SyiCQmTJhgNpsPHDggiko9k6wEX9MTJoGg1+udMGHCyy+/LPS0l7MVcJtMJrvdXl1dLZQoIaMULAghpk4yRmsSWAOAz+ebPXv2j370I/FM8mD1e+RIuPa9e/euXr3a4/E4nU5xqsmsI2AKh8MA8OCDDz722GOiUZzMLRD6dfz48Z/97GeNjY0pKSn9moQRrCORCAAsWrTo61//ekI9dkDAMvDyeDwbN2784IMPFEWx2Wwi9rvs3RQC6bouClhlZWWPPvpoaWmpuC/JiytgDYVCGzdu/NOf/iT678LrXbZUD3GWQdjT4uLiJUuWFBcX95f11YMFnTOSCKFTp0798Y9/rKysbG9vFwGeMcoCnbM+uq5zzlNSUkpLS++9994vf/nLQhmvQlzjrZMnT7755ptVVVWhUEiWZXHv4hcUcZamaaqqSpI0ZsyYBx54YMaMGcKKXafJP4PEYRpW4PDhw0ePHhXzkiKSEG4xLS1txIgR48eP/9KXvjRs2LCEF6+Rb1NT0549ew4dOtTY2BgMBjVNM7YjSZLNZhs6dGhxcfHUqVNLS0tFw+JaWF/rD/f0jJ5VVY1EIrquY4ytVqvVau3l4S+Kr8/na2lpuXjxomhKWywWt9udlZU1dOhQw9KL0P9amH4xv3IkRIFOO9pzY+I8v/CvJiWzskh6vpAT+uJ/Eqqngf9i178S0wQbb1RyvkAuN/S3lW82uvE/hX0T0SBY/aBBsPpBg2D1gwbB6gcNgtUPGgSrHzQIVj9oEKx+0CBY/aD/A/ORNiwv2PAfAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE5LTA2LTEzVDAzOjE3OjE2LTA0OjAwj3mANAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOS0wNi0xM1QwMzoxNzoxNS0wNDowMM/MIhUAAAAASUVORK5CYII=', "is_valid": true, "label": "Webhook", "environment": "onprem", + "description": "Simple HTTP webhook", + "long_description": "Execute a workflow with an unauthicated POST request", }, { "name": "User Input", @@ -1534,6 +1537,7 @@ const AngularWorkflow = (props) => { "is_valid": true, "label": "User input", "environment": "onprem", + "long_description": "Take user input to continue execution", }, { "name": "Schedule", @@ -1546,6 +1550,7 @@ const AngularWorkflow = (props) => { "is_valid": true, "label": "Schedule", "environment": "onprem", + "long_description": "Create a schedule based on cron", }, { "name": "Email", @@ -1558,6 +1563,7 @@ const AngularWorkflow = (props) => { "label": "Email", "environment": "cloud", "large_image": 'data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoM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', + "long_description": "Execute a workflow when you get an email", }] return (