From 7c940e57d477cf6c44c7a54208b70297bd6cb409 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 22 Jun 2020 13:17:14 +0200 Subject: [PATCH 01/14] Added default user option and user control --- .env | 5 + backend/go-app/main.go | 425 ++++++++++++++++---------------- docker-compose.yml | 3 + frontend/src/Admin.js | 11 +- frontend/src/AngularWorkflow.js | 45 ++-- 5 files changed, 251 insertions(+), 238 deletions(-) diff --git a/.env b/.env index 2312df7d..c8986223 100644 --- a/.env +++ b/.env @@ -7,6 +7,11 @@ APP_DOWNLOAD_LOCATION=https://github.com/frikky/shuffle-apps APP_DOWNLOAD_AUTH_USERNAME="" APP_DOWNLOAD_AUTH_PASSWORD="" +# User config for first load +SHUFFLE_DEFAULT_USERNAME="" +SHUFFLE_DEFAULT_PASSWORD="" +SHUFFLE_DEFAULT_APIKEY="" + # Local location of your app directory. Can't use ~/ APP_HOTLOAD_LOCATION=./shuffle-apps diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 5326e38b..6b89fa6b 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -995,6 +995,119 @@ func handleSetEnvironments(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": true}`)) } +func createNewUser(username, password, role, apikey string) error { + // Returns false if there is an issue + // Use this for register + err := checkPasswordStrength(password) + if err != nil { + log.Printf("Bad password strength: %s", err) + return err + } + + err = checkUsername(username) + if err != nil { + log.Printf("Bad Username strength: %s", err) + return err + } + + ctx := context.Background() + q := datastore.NewQuery("Users").Filter("Username =", username) + var users []User + _, err = dbclient.GetAll(ctx, q, &users) + if err != nil { + log.Printf("Failed getting user for registration: %s", err) + return err + } + + if len(users) > 0 { + return errors.New(fmt.Sprintf("Username %s already exists", username)) + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 8) + if err != nil { + log.Printf("Wrong password for %s: %s", username, err) + return err + } + + newUser := new(User) + newUser.Username = username + newUser.Password = string(hashedPassword) + newUser.Verified = false + newUser.CreationTime = time.Now().Unix() + newUser.Active = true + newUser.Orgs = []string{"default"} + + // FIXME - Remove this later + if role == "admin" { + newUser.Role = "admin" + newUser.Roles = []string{"admin"} + } else { + newUser.Role = "user" + newUser.Roles = []string{"user"} + } + + if len(apikey) > 0 { + newUser.ApiKey = apikey + } + + // set limits + newUser.Limits.DailyApiUsage = 100 + newUser.Limits.DailyWorkflowExecutions = 1000 + newUser.Limits.DailyCloudExecutions = 100 + newUser.Limits.DailyTriggers = 20 + newUser.Limits.DailyMailUsage = 100 + newUser.Limits.MaxTriggers = 10 + newUser.Limits.MaxWorkflows = 10 + + // Set base info for the user + newUser.Executions.TotalApiUsage = 0 + newUser.Executions.TotalWorkflowExecutions = 0 + newUser.Executions.TotalAppExecutions = 0 + newUser.Executions.TotalCloudExecutions = 0 + newUser.Executions.TotalOnpremExecutions = 0 + newUser.Executions.DailyApiUsage = 0 + newUser.Executions.DailyWorkflowExecutions = 0 + newUser.Executions.DailyAppExecutions = 0 + newUser.Executions.DailyCloudExecutions = 0 + newUser.Executions.DailyOnpremExecutions = 0 + + verifyToken := uuid.NewV4() + ID := uuid.NewV4() + newUser.Id = ID.String() + newUser.VerificationToken = verifyToken.String() + err = setUser(ctx, newUser) + if err != nil { + log.Printf("Error adding User %s: %s", username, err) + return err + } + url := fmt.Sprintf("https://shuffler.io/register/%s", verifyToken.String()) + const verifyMessage = ` +Registration URL :) + +%s + ` + addr := newUser.Username + + msg := &mail.Message{ + Sender: "Shuffle ", + To: []string{addr}, + Subject: "Verify your username - Shuffle", + Body: fmt.Sprintf(verifyMessage, url), + } + + log.Println(msg.Body) + if err := mail.Send(ctx, msg); err != nil { + log.Printf("Couldn't send email: %v", err) + } + + err = increaseStatisticsField(ctx, "successful_register", username, 1) + if err != nil { + log.Printf("Failed to increase total apps loaded stats: %s", err) + } + + return nil +} + func handleRegister(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -1030,128 +1143,21 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) { return } - // Returns false if there is an issue - // Use this for register - err = checkPasswordStrength(data.Password) - if err != nil { - log.Printf("Bad password strength: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - err = checkUsername(data.Username) - if err != nil { - log.Printf("Bad Username strength: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - ctx := context.Background() - q := datastore.NewQuery("Users").Filter("Username =", data.Username) - var users []User - _, err = dbclient.GetAll(ctx, q, &users) - if err != nil { - log.Printf("Failed getting user for registration: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting username"}`))) - return - } - - if len(users) > 0 { - log.Printf("Username %s exists and can't register", data.Username) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(data.Password), 8) - if err != nil { - log.Printf("Wrong password for %s: %s", data.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) - return - } - - newUser := new(User) - newUser.Username = data.Username - newUser.Password = string(hashedPassword) - newUser.Verified = false - newUser.CreationTime = time.Now().Unix() - newUser.Active = true - newUser.Orgs = []string{"default"} - - // FIXME - Remove this later + role := "user" if count == 0 { - newUser.Role = "admin" - newUser.Roles = []string{"admin"} - } else { - newUser.Role = "user" - newUser.Roles = []string{"user"} + role = "admin" } - - // set limits - newUser.Limits.DailyApiUsage = 100 - newUser.Limits.DailyWorkflowExecutions = 1000 - newUser.Limits.DailyCloudExecutions = 100 - newUser.Limits.DailyTriggers = 20 - newUser.Limits.DailyMailUsage = 100 - newUser.Limits.MaxTriggers = 10 - newUser.Limits.MaxWorkflows = 10 - - // Set base info for the user - newUser.Executions.TotalApiUsage = 0 - newUser.Executions.TotalWorkflowExecutions = 0 - newUser.Executions.TotalAppExecutions = 0 - newUser.Executions.TotalCloudExecutions = 0 - newUser.Executions.TotalOnpremExecutions = 0 - newUser.Executions.DailyApiUsage = 0 - newUser.Executions.DailyWorkflowExecutions = 0 - newUser.Executions.DailyAppExecutions = 0 - newUser.Executions.DailyCloudExecutions = 0 - newUser.Executions.DailyOnpremExecutions = 0 - - addr := newUser.Username - - verifyToken := uuid.NewV4() - ID := uuid.NewV4() - newUser.Id = ID.String() - newUser.VerificationToken = verifyToken.String() - err = setUser(ctx, newUser) + err = createNewUser(data.Username, data.Password, role, "") if err != nil { - log.Printf("Error adding User %s: %s", data.Username, err) + log.Printf("Failed registering user: %s", err) resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) return } - url := fmt.Sprintf("https://shuffler.io/register/%s", verifyToken.String()) - const verifyMessage = ` -Registration URL :) - -%s - ` - - msg := &mail.Message{ - Sender: "Shuffle ", - To: []string{addr}, - Subject: "Verify your username - Shuffle", - Body: fmt.Sprintf(verifyMessage, url), - } - - log.Println(msg.Body) - if err := mail.Send(ctx, msg); err != nil { - log.Printf("Couldn't send email: %v", err) - } resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) log.Printf("%s Successfully registered.", data.Username) - - err = increaseStatisticsField(ctx, "successful_register", data.Username, 1) - if err != nil { - log.Printf("Failed to increase total apps loaded stats: %s", err) - } } func handleCookie(request *http.Request) bool { @@ -1285,56 +1291,73 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { return } - c, err := request.Cookie("session_token") + userInfo, err := handleApiAuthentication(resp, request) if err != nil { - log.Printf("User doesn't have sessiontoken, on apigen: %s", err) + log.Printf("Api authentication failed in apigen: %s", err) resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + resp.Write([]byte(`{"success": false}`)) return } + log.Printf("APIKEY") ctx := context.Background() - sessionToken := c.Value - session, err := getSession(ctx, sessionToken) - if err != nil { - log.Printf("Session %#v doesn't exist (api gen): %s", session, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return + if request.Method == "GET" { + newUserInfo, err := generateApikey(ctx, userInfo) + if err != nil { + log.Printf("Failed to generate apikey for user %s: %s", userInfo.Username, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": ""}`)) + return + } + userInfo = newUserInfo + log.Printf("Updated apikey for user %s", userInfo.Username) + } else if request.Method == "POST" { + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Println("Failed reading body") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Missing field: user_id"}`))) + return + } + + type userId struct { + UserId string `json:"user_id"` + } + + var t userId + err = json.Unmarshal(body, &t) + if err != nil { + log.Printf("Failed unmarshaling userId: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unmarshaling. Missing field: user_id"}`))) + return + } + + if userInfo.Role != "admin" { + log.Printf("%s tried and failed to change apikey for %s", userInfo.Username, t.UserId) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "You need to be admin to change others' apikey"}`))) + return + } + + foundUser, err := getUser(ctx, t.UserId) + if err != nil { + log.Printf("Can't find user %s (apikey gen)", t.UserId, err) + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) + return + } + + newUserInfo, err := generateApikey(ctx, *foundUser) + if err != nil { + log.Printf("Failed to generate apikey for user %s: %s", foundUser.Username, err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + foundUser = &newUserInfo } - // Get session first - // Should basically never happen - userInfo, err := getUser(ctx, session.Id) - if err != nil { - log.Printf("Username %s doesn't exist (apigen): %s", session.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - // Delete old apikey from cache - //memcache.Delete(ctx, userInfo.ApiKey) - - if session.Session != userInfo.Session { - log.Printf("Session %s is not the latest. %s", session.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - newUserInfo, err := generateApikey(ctx, *userInfo) - if err != nil { - log.Printf("Failed to generate apikey for user %s: %s", session.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - userInfo = &newUserInfo - - //memcache.Delete(request.Context(), sessionToken) - - log.Printf("Updated apikey for user %s", userInfo.Username) resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, userInfo.Username, userInfo.Verified, userInfo.ApiKey))) } @@ -1345,44 +1368,16 @@ func handleSettings(resp http.ResponseWriter, request *http.Request) { return } - c, err := request.Cookie("session_token") + userInfo, err := handleApiAuthentication(resp, request) if err != nil { - log.Printf("User doesn't have sessiontoken, on getsettings: %s", err) + log.Printf("Api authentication failed in apigen: %s", err) resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) - return - } - - ctx := context.Background() - sessionToken := c.Value - session, err := getSession(ctx, sessionToken) - if err != nil { - log.Printf("Session %#v doesn't exist (settings): %s", session, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - // Get session first - // Should basically never happen - UserInfo, err := getUser(ctx, session.Id) - if err != nil { - log.Printf("Username %s doesn't exist (settings): %s", session.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - //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) - resp.Write([]byte(`{"success": false, "reason": ""}`)) + resp.Write([]byte(`{"success": false}`)) return } resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, UserInfo.Username, UserInfo.Verified, UserInfo.ApiKey))) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, userInfo.Username, userInfo.Verified, userInfo.ApiKey))) } func handleInfo(resp http.ResponseWriter, request *http.Request) { @@ -1391,21 +1386,16 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { return } - // Should compare with local storage first - c, err := request.Cookie("session_token") + userInfo, err := handleApiAuthentication(resp, request) if err != nil { + log.Printf("Api authentication failed in handleInfo: %s", err) resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + resp.Write([]byte(`{"success": false}`)) return } - // FIXME - check memcache here - // Get the item from the memcache ctx := context.Background() - - sessionToken := c.Value - //log.Printf("Found session %s", sessionToken) - session, err := getSession(ctx, sessionToken) + session, err := getSession(ctx, userInfo.Session) if err != nil { log.Printf("Session %#v doesn't exist: %s", session, err) resp.WriteHeader(401) @@ -1413,17 +1403,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { return } - // Get session first - // Should basically never happen - userInfo, err := getUser(ctx, session.Id) - if err != nil { - log.Printf("Username %s doesn't exist (info): %s", session.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - // This is a long check to see if an inactive admin can access the portal + // This is a long check to see if an inactive admin can access the site if !userInfo.Active { if userInfo.Role == "admin" { ctx := context.Background() @@ -1478,7 +1458,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { //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) + log.Printf("Session %s is not the same as %s for %s. %s", userInfo.Session, session.Session, userInfo.Username, err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": ""}`)) return @@ -5966,18 +5946,18 @@ func runInit(ctx context.Context) { } // Fix active users etc - log.Printf("Reformatting users") + log.Printf("Checking users") q := datastore.NewQuery("Users").Filter("active =", true) - var users []User - _, err = dbclient.GetAll(ctx, q, &users) + var activeusers []User + _, err = dbclient.GetAll(ctx, q, &activeusers) if err != nil { - log.Printf("Error getting users apikey (runinit): %s", err) + log.Printf("Error getting users during init: %s", err) } else { - if len(users) == 0 { + q := datastore.NewQuery("Users") + var users []User + _, err := dbclient.GetAll(ctx, q, &users) + if len(activeusers) == 0 && len(users) > 0 { log.Printf("No active users found - setting ALL to active") - q := datastore.NewQuery("Users") - var users []User - _, err := dbclient.GetAll(ctx, q, &users) if err == nil { for _, user := range users { user.Active = true @@ -6006,6 +5986,19 @@ func runInit(ctx context.Context) { } } } + } else if len(users) == 0 { + log.Printf("Trying to set up user based on environments DEFAULT_USERNAME & DEFAULT_PASSWORD") + username := os.Getenv("SHUFFLE_DEFAULT_USERNAME") + password := os.Getenv("SHUFFLE_DEFAULT_PASSWORD") + if len(username) == 0 || len(password) == 0 { + log.Printf("DEFAULT_USERNAME and DEFAULT_PASSWORD not defined as environments. Running without default user.") + } else { + apikey := os.Getenv("SHUFFLE_DEFAULT_APIKEY") + err = createNewUser(username, password, "admin", apikey) + if err != nil { + log.Printf("Failed to create default user %s: %s", username, err) + } + } } } @@ -6175,7 +6168,7 @@ func init() { r.HandleFunc("/api/v1/getusers", handleGetUsers).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/getinfo", handleInfo).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/getsettings", handleSettings).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/generateapikey", handleApiGeneration).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/generateapikey", handleApiGeneration).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/getenvironments", handleGetEnvironments).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/setenvironments", handleSetEnvironments).Methods("PUT", "OPTIONS") diff --git a/docker-compose.yml b/docker-compose.yml index 288063bb..25c4f51c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,9 @@ services: - DATASTORE_EMULATOR_HOST=shuffle-database:8000 - APP_DOWNLOAD_LOCATION=${APP_DOWNLOAD_LOCATION} - APP_HOTLOAD_FOLDER=/shuffle-apps + - SHUFFLE_DEFAULT_USERNAME=${SHUFFLE_DEFAULT_USERNAME} + - SHUFFLE_DEFAULT_PASSWORD=${SHUFFLE_DEFAULT_PASSWORD} + - SHUFFLE_DEFAULT_APIKEY=${SHUFFLE_DEFAULT_APIKEY} restart: unless-stopped depends_on: - database diff --git a/frontend/src/Admin.js b/frontend/src/Admin.js index 06f14f64..8809dd1d 100644 --- a/frontend/src/Admin.js +++ b/frontend/src/Admin.js @@ -319,14 +319,17 @@ const Admin = (props) => { modalUser[field] = value } - const generateApikey = () => { + const generateApikey = (userId) => { + const data = {"user_id": userId} + fetch(globalUrl+"/api/v1/generateapikey", { - method: 'GET', + method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, - credentials: "include", + body: JSON.stringify(data), + credentials: "include", }) .then((response) => { if (response.status !== 200) { @@ -407,7 +410,7 @@ const Admin = (props) => { style={{}} variant="outlined" color="primary" - onClick={() => generateApikey(selectedUser)} + onClick={() => generateApikey(selectedUser.id)} > Get new API key diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index e1f8bf62..87670cd2 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -845,6 +845,7 @@ const AngularWorkflow = (props) => { setRightSideBarOpen(true) } + // Checks for errors in edges when they're added const onEdgeAdded = (event) => { setLastSaved(false) const edge = event.target.data() @@ -856,18 +857,13 @@ const AngularWorkflow = (props) => { var found = false for (var key in workflow.branches) { if (workflow.branches[key].destination_id === edge.source && workflow.branches[key].source_id === edge.target) { - alert.error("A pointer in the opposite direction already exists") + alert.error("A branch in the opposite direction already exists") event.target.remove() found = true break } else if (workflow.branches[key].destination_id === edge.target && workflow.branches[key].source_id === edge.source) { - // Checks if NOT trigger - //workflow.triggers.find(data => data.id === workflow.branches[key].source_id).length === 0) - const triggercheck = workflow.triggers.find(data => data.id === workflow.branches[key].source_id) - if (workflow.triggers !== undefined && triggercheck !== undefined && triggercheck.length === 0) { - alert.error("That pointer already exists") - event.target.remove() - } + alert.error("That branch already exists") + event.target.remove() found = true break } else if (edge.target === workflow.start) { @@ -878,16 +874,29 @@ const AngularWorkflow = (props) => { found = true break } + } else if (edge.source === workflow.branches[key].source_id) { + // FIXME: Verify multi-target for triggers + // 1. Check if destination exists + // 2. Check if source is a trigger + // targetnode = workflow.triggers.findIndex(data => data.id === edge.source) + // console.log("Destination: ", edge.target) + // console.log("CHECK SOURCE IF ITS A TRIGGER: ", targetnode) + // if (targetnode !== -1) { + // alert.error("Triggers can only target one target (startnode)") + // event.target.remove() + // found = true + // break + // } } else { // Find the targetnode and check if its a trigger // FIXME - do this for both actions and other types? - //targetnode = workflow.triggers.findIndex(data => data.id === edge.target) - //if (targetnode !== -1) { - // alert.error("Can't have triggers as target") - // event.target.remove() - // found = true - // break - //} + targetnode = workflow.triggers.findIndex(data => data.id === edge.target) + if (targetnode !== -1) { + alert.error("Can't have triggers as target of branch") + event.target.remove() + found = true + break + } } } @@ -901,10 +910,10 @@ const AngularWorkflow = (props) => { if (!found) { newbranch["hasErrors"] = false - } - workflow.branches.push(newbranch) - setWorkflow(workflow) + workflow.branches.push(newbranch) + setWorkflow(workflow) + } } const onNodeAdded = (event) => { From ec9254ffa25218ab063ef7e86a45fac400c87fd2 Mon Sep 17 00:00:00 2001 From: frikky Date: Mon, 22 Jun 2020 13:39:51 +0200 Subject: [PATCH 02/14] added ability to change other uses --- backend/go-app/main.go | 87 +++++++++++++++++++++++++++++++++++++++++- frontend/src/Admin.js | 74 ++++++++++++++++++++++++++++++----- 2 files changed, 150 insertions(+), 11 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 6b89fa6b..e61fd2fa 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -1285,6 +1285,88 @@ func generateApikey(ctx context.Context, userInfo User) (User, error) { return userInfo, nil } +func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + userInfo, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in apigen: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Println("Failed reading body") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Missing field: user_id"}`))) + return + } + + type newUserStruct struct { + Role string `json:"role"` + UserId string `json:"user_id"` + } + + ctx := context.Background() + var t newUserStruct + err = json.Unmarshal(body, &t) + if err != nil { + log.Printf("Failed unmarshaling userId: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unmarshaling. Missing field: user_id"}`))) + return + } + + if userInfo.Role != "admin" { + log.Printf("%s tried to update user %s", userInfo.Username, t.UserId) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "You need to be admin to change other users"}`))) + return + } + + foundUser, err := getUser(ctx, t.UserId) + if err != nil { + log.Printf("Can't find user %s (apikey gen)", t.UserId, err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false}`))) + return + } + + if t.Role != "admin" && t.Role != "user" { + log.Printf("%s tried and failed to update user %s", userInfo.Username, t.UserId) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can only change to role user and admin"}`))) + return + } else { + // Same user - can't edit yourself + if userInfo.Id == t.UserId { + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't update the role of your own user"}`))) + return + } + + log.Printf("Updated user %s from %s to %s", foundUser.Username, foundUser.Role, t.Role) + foundUser.Role = t.Role + foundUser.Roles = []string{t.Role} + } + + err = setUser(ctx, foundUser) + if err != nil { + log.Printf("Error patching user %s: %s", foundUser.Username, err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false}`))) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) +} + func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -1343,8 +1425,8 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { foundUser, err := getUser(ctx, t.UserId) if err != nil { log.Printf("Can't find user %s (apikey gen)", t.UserId, err) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false}`))) return } @@ -6158,6 +6240,7 @@ func init() { r.HandleFunc("/api/v1/users/getinfo", handleInfo).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/getsettings", handleSettings).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/generateapikey", handleApiGeneration).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/users/updateuser", handleUpdateUser).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/users/{user}", deleteUser).Methods("DELETE", "OPTIONS") // General - duplicates and old. diff --git a/frontend/src/Admin.js b/frontend/src/Admin.js index 8809dd1d..902bf3b9 100644 --- a/frontend/src/Admin.js +++ b/frontend/src/Admin.js @@ -2,6 +2,8 @@ import React, { useEffect} from 'react'; import {Link} from 'react-router-dom'; import Paper from '@material-ui/core/Paper'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; import List from '@material-ui/core/List'; import Divider from '@material-ui/core/Divider'; import TextField from '@material-ui/core/TextField'; @@ -319,6 +321,43 @@ const Admin = (props) => { modalUser[field] = value } + const setUser = (userId, field, value) => { + const data = {"user_id": userId} + data[field] = value + console.log("DATA: ", data) + + fetch(globalUrl+"/api/v1/users/updateuser", { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!") + } else { + getUsers() + } + + return response.json() + }) + .then((responseJson) => { + if (!responseJson.success && responseJson.reason !== undefined) { + alert.error("Failed setting user: "+responseJson.reason) + } else { + alert.success("Set the user field "+field+" to "+value) + } + }) + .catch(error => { + console.log(error) + }); + } + + + const generateApikey = (userId) => { const data = {"user_id": userId} @@ -543,10 +582,6 @@ const Admin = (props) => { primary="API key" style={{minWidth: 350, maxWidth: 350, overflow: "hidden"}} /> - { style={{maxWidth: 350, minWidth: 350,}} /> - { + console.log("VALUE: ", e.target.value) + setUser(data.id, "role", e.target.value) + }} + style={{backgroundColor: surfaceColor, color: "white", height: "50px"}} + > + + Admin + + + User + + style={{minWidth: 150, maxWidth: 150}} /> Date: Mon, 22 Jun 2020 14:24:33 +0200 Subject: [PATCH 03/14] #63: Added general user editing ability for users --- backend/go-app/main.go | 123 ++++++++++++++++++++++++++++++++++++-- backend/go-app/walkoff.go | 78 ------------------------ frontend/src/Admin.js | 5 +- frontend/src/App.js | 2 +- frontend/src/LoginPage.js | 4 +- 5 files changed, 124 insertions(+), 88 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index e61fd2fa..9774b606 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -838,6 +838,84 @@ func checkPasswordStrength(password string) error { return nil } +func deleteUser(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + user, userErr := handleApiAuthentication(resp, request) + if userErr != nil { + log.Printf("Api authentication failed in edit workflow: %s", userErr) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if user.Role != "admin" { + log.Printf("Wrong user (%s) when deleting - must be admin", user.Username) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Must be admin"}`)) + return + } + + location := strings.Split(request.URL.String(), "/") + var userId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + userId = location[4] + } + + if userId == user.Id { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Can't deactivate yourself"}`)) + return + } + + ctx := context.Background() + q := datastore.NewQuery("Users").Filter("id =", userId) + var users []User + _, err := dbclient.GetAll(ctx, q, &users) + if err != nil { + log.Printf("Error getting users apikey (deleteuser): %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Failed getting users for verification"}`)) + return + } + + if len(users) != 1 { + log.Printf("Found too many users!") + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Backend error: too many or too few users with id %s: %d"}`, userId, len(users)))) + return + } + + // Invert. No user deletion. + if users[0].Active { + users[0].Active = false + } else { + users[0].Active = true + } + + err = setUser(ctx, &users[0]) + if err != nil { + log.Printf("Failed swapping active for user %s (%s)", users[0].Username, users[0].Id) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": true"}`))) + return + } + + log.Printf("Successfully inverted %s", users[0].Username) + + resp.WriteHeader(200) + resp.Write([]byte(`{"success": true}`)) +} + // No more emails :) func checkUsername(Username string) error { // Stupid first check of email loool @@ -1308,8 +1386,9 @@ func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { } type newUserStruct struct { - Role string `json:"role"` - UserId string `json:"user_id"` + Role string `json:"role"` + Username string `json:"username"` + UserId string `json:"user_id"` } ctx := context.Background() @@ -1331,7 +1410,7 @@ func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { foundUser, err := getUser(ctx, t.UserId) if err != nil { - log.Printf("Can't find user %s (apikey gen)", t.UserId, err) + log.Printf("Can't find user %s (update user): %s", t.UserId, err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false}`))) return @@ -1355,6 +1434,33 @@ func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { foundUser.Roles = []string{t.Role} } + if len(t.Username) > 0 { + q := datastore.NewQuery("Users").Filter("username =", t.Username) + var users []User + _, err = dbclient.GetAll(ctx, q, &users) + if err != nil { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Failed getting users when updating user"}`)) + return + } + + found := false + for _, item := range users { + if item.Username == t.Username { + found = true + break + } + } + + if found { + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "User with username %s already exists"}`, t.Username))) + return + } + + foundUser.Username = t.Username + } + err = setUser(ctx, foundUser) if err != nil { log.Printf("Error patching user %s: %s", foundUser.Username, err) @@ -1368,6 +1474,7 @@ func handleUpdateUser(resp http.ResponseWriter, request *http.Request) { } func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { + log.Printf("APIGEN!") cors := handleCors(resp, request) if cors { return @@ -1394,6 +1501,7 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { userInfo = newUserInfo log.Printf("Updated apikey for user %s", userInfo.Username) } else if request.Method == "POST" { + log.Printf("Handling post!") body, err := ioutil.ReadAll(request.Body) if err != nil { log.Println("Failed reading body") @@ -1424,7 +1532,7 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { foundUser, err := getUser(ctx, t.UserId) if err != nil { - log.Printf("Can't find user %s (apikey gen)", t.UserId, err) + log.Printf("Can't find user %s (apikey gen): %s", t.UserId, err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false}`))) return @@ -1438,6 +1546,10 @@ func handleApiGeneration(resp http.ResponseWriter, request *http.Request) { return } foundUser = &newUserInfo + + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "username": "%s", "verified": %t, "apikey": "%s"}`, foundUser.Username, foundUser.Verified, foundUser.ApiKey))) + return } resp.WriteHeader(200) @@ -6232,6 +6344,7 @@ func init() { r.HandleFunc("/functions/outlook/getFolders", handleGetOutlookFolders).Methods("GET", "OPTIONS") // Make user related locations + r.HandleFunc("/api/v1/users/generateapikey", handleApiGeneration).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/users/login", handleLogin).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users/logout", handleLogout).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users/register", handleRegister).Methods("POST", "OPTIONS") @@ -6239,9 +6352,9 @@ func init() { r.HandleFunc("/api/v1/users/getusers", handleGetUsers).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/getinfo", handleInfo).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/getsettings", handleSettings).Methods("GET", "OPTIONS") - r.HandleFunc("/api/v1/users/generateapikey", handleApiGeneration).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/updateuser", handleUpdateUser).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/users/{user}", deleteUser).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/users", handleGetUsers).Methods("GET", "OPTIONS") // General - duplicates and old. r.HandleFunc("/api/v1/login", handleLogin).Methods("POST", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 36ab5d2a..b7541c87 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2929,84 +2929,6 @@ func setWorkflow(ctx context.Context, workflow Workflow, id string) error { return nil } -func deleteUser(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, userErr := handleApiAuthentication(resp, request) - if userErr != nil { - log.Printf("Api authentication failed in edit workflow: %s", userErr) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - if user.Role != "admin" { - log.Printf("Wrong user (%s) when deleting - must be admin", user.Username) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Must be admin"}`)) - return - } - - location := strings.Split(request.URL.String(), "/") - var userId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - userId = location[4] - } - - if userId == user.Id { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Can't deactivate yourself"}`)) - return - } - - ctx := context.Background() - q := datastore.NewQuery("Users").Filter("id =", userId) - var users []User - _, err := dbclient.GetAll(ctx, q, &users) - if err != nil { - log.Printf("Error getting users apikey (deleteuser): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Failed getting users for verification"}`)) - return - } - - if len(users) != 1 { - log.Printf("Found too many users!") - resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Backend error: too many users"}`)) - return - } - - // Invert. No user deletion. - if users[0].Active { - users[0].Active = false - } else { - users[0].Active = true - } - - err = setUser(ctx, &users[0]) - if err != nil { - log.Printf("Failed swapping active for user %s (%s)", users[0].Username, users[0].Id) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": true"}`))) - return - } - - log.Printf("Successfully inverted %s", users[0].Username) - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { diff --git a/frontend/src/Admin.js b/frontend/src/Admin.js index 902bf3b9..2c76392b 100644 --- a/frontend/src/Admin.js +++ b/frontend/src/Admin.js @@ -72,7 +72,7 @@ const Admin = (props) => { const onPasswordChange = () => { const data = {"username": selectedUser.username, "newpassword": newPassword} - const url = globalUrl+'/api/v1/passwordchange'; + const url = globalUrl+'/api/v1/users/passwordchange'; fetch(url, { mode: 'cors', method: 'POST', @@ -135,7 +135,7 @@ const Admin = (props) => { // Just use this one? var data = {"username": data.Username, "password": data.Password} var baseurl = globalUrl - const url = baseurl+'/api/v1/register'; + const url = baseurl+'/api/v1/users/register'; fetch(url, { method: 'POST', credentials: "include", @@ -296,6 +296,7 @@ const Admin = (props) => { return response.json() }) .then((responseJson) => { + console.log(responseJson) setUsers(responseJson) }) .catch(error => { diff --git a/frontend/src/App.js b/frontend/src/App.js index fd8d20ab..8eb7f108 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -84,7 +84,7 @@ const App = (message, props) => { const checkLogin = () => { var baseurl = globalUrl - fetch(baseurl+"/api/v1/getinfo", { + fetch(baseurl+"/api/v1/users/getinfo", { credentials: "include", headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/LoginPage.js b/frontend/src/LoginPage.js index 7ff22e80..49e22f55 100644 --- a/frontend/src/LoginPage.js +++ b/frontend/src/LoginPage.js @@ -92,7 +92,7 @@ const LoginDialog = props => { var data = {"username": username, "password": password} var baseurl = globalUrl if (register) { - var url = baseurl+'/api/v1/login'; + var url = baseurl+'/api/v1/users/login'; fetch(url, { mode: 'cors', method: 'POST', @@ -123,7 +123,7 @@ const LoginDialog = props => { setLoginInfo("Error in userdata: " + error) }); } else { - url = baseurl+'/api/v1/register'; + url = baseurl+'/api/v1/users/register'; fetch(url, { method: 'POST', body: JSON.stringify(data), From 0d17de238b44327fe946313fa1057be4443b3af4 Mon Sep 17 00:00:00 2001 From: frikky Date: Tue, 23 Jun 2020 20:04:00 +0200 Subject: [PATCH 04/14] Bugfixes towards blogpost 4 --- .env | 12 +++++----- backend/app_sdk/app_base.py | 34 ++++++++++++++++++++------- backend/go-app/main.go | 30 +++++++++++++---------- backend/go-app/walkoff.go | 16 +++++++++---- docker-compose.yml | 6 ++--- frontend/src/Admin.js | 2 +- frontend/src/AngularWorkflow.js | 18 +++++++------- frontend/src/defaultCytoscapeStyle.js | 2 ++ functions/onprem/orborus/orborus.go | 5 ++-- functions/onprem/worker/worker.go | 8 ++++++- 10 files changed, 86 insertions(+), 47 deletions(-) diff --git a/.env b/.env index c8986223..ee13a292 100644 --- a/.env +++ b/.env @@ -4,13 +4,13 @@ ENVIRONMENT_NAME=Shuffle # Remote github config for first load APP_DOWNLOAD_LOCATION=https://github.com/frikky/shuffle-apps -APP_DOWNLOAD_AUTH_USERNAME="" -APP_DOWNLOAD_AUTH_PASSWORD="" +APP_DOWNLOAD_AUTH_USERNAME= +APP_DOWNLOAD_AUTH_PASSWORD= -# User config for first load -SHUFFLE_DEFAULT_USERNAME="" -SHUFFLE_DEFAULT_PASSWORD="" -SHUFFLE_DEFAULT_APIKEY="" +# User config for first load. Username & PW: min length 3 +SHUFFLE_DEFAULT_USERNAME= +SHUFFLE_DEFAULT_PASSWORD= +SHUFFLE_DEFAULT_APIKEY= # Local location of your app directory. Can't use ~/ APP_HOTLOAD_LOCATION=./shuffle-apps diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 3f7a788f..9e45426f 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -384,14 +384,27 @@ class AppBase: if value == "#": # FIXME - not recursive - should go deeper if there are more # - print("HANDLE RECURSIVE LOOP ") + print("HANDLE RECURSIVE LOOP OF %s" % basejson) returnlist = [] - for innervalue in basejson: - #print("Value: %s" % value[parsersplit[cnt+1]]) - returnlist.append(innervalue[parsersplit[cnt+1]]) + try: + for innervalue in basejson: + print("Value: %s" % value[parsersplit[cnt+1]]) + returnlist.append(innervalue[parsersplit[cnt+1]]) + except IndexError as e: + print("Indexerror inner: %s" % e) + # Basically means its a normal list, not a crazy one :) + # Custom format for ${name[0,1,2,...]}$ + indexvalue = "${NO_SPLITTER%s}$" % json.dumps(basejson) + if len(returnlist) > 0: + indexvalue = "${NO_SPLITTER%s}$" % json.dumps(returnlist) + + print("INDEXVAL: ", indexvalue) + return indexvalue # Example format: ${[]}$ - return "${%s%s}$" % (parsersplit[cnt+1], json.dumps(returnlist)) + parseditem = "${%s%s}$" % (parsersplit[cnt+1], json.dumps(returnlist)) + print("PARSED LOOP ITEM: %s" % parseditem) + return parseditem else: if isinstance(basejson[value], str): @@ -405,9 +418,8 @@ class AppBase: basejson = basejson[value] except KeyError as e: + print("Lower keyerror: %s" % e) return "KeyError: %s" % e - except IndexError as e: - return "IndexError: %s" % e return basejson @@ -424,7 +436,7 @@ class AppBase: #self.logger.debug(f"\n\nHandle static data with JSON: {data}\n\n") #self.logger.info("STATIC PARSED: %s" % actualitem) if len(actualitem) > 0: - print("ACTUAL: %s", actualitem) + print("ACTUAL: ", actualitem) for replace in actualitem: try: to_be_replaced = replace[0] @@ -488,8 +500,10 @@ class AppBase: else: fullname += parameter["action_field"] + self.logger.info("PRE Fullname: %s" % fullname) + if parameter["value"].startswith(jsonparsevalue): - fullname += parameter["value"][2:] + fullname += parameter["value"][1:] #else: # fullname = "$%s" % parameter["action_field"] @@ -699,8 +713,10 @@ class AppBase: raise "Value check error: %s" % Exception(check) # Custom format for ${name[0,1,2,...]}$ + #submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" actualitem = re.findall(submatch, value, re.MULTILINE) + print("Multicheck: %s", actualitem) if len(actualitem) > 0: multiexecution = True diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 9774b606..f36fa593 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -812,8 +812,8 @@ func parseLoginParameters(resp http.ResponseWriter, request *http.Request) (logi // Removed for localhost func checkPasswordStrength(password string) error { // Check password strength here - if len(password) < 10 { - return errors.New("Minimum password length is 10.") + if len(password) < 3 { + return errors.New("Minimum password length is 3.") } //if len(password) > 128 { @@ -923,8 +923,8 @@ func checkUsername(Username string) error { // return errors.New("Invalid Username") //} - if len(Username) < 4 { - return errors.New("Minimum Username length is 4") + if len(Username) < 3 { + return errors.New("Minimum Username length is 3") } return nil @@ -1939,7 +1939,7 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { if len(users) != 1 { log.Printf(`Found multiple users with the same username: %s: %d`, t.Username, len(users)) resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found multiple users with the same username: %s"}`, t.Username))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found %d users with the same username: %s (%d)"}`, len(users), t.Username))) return } @@ -2233,7 +2233,8 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - q := datastore.NewQuery("Users").Filter("Username =", strings.ToLower(data.Username)) + log.Printf("Username: %s", data.Username) + q := datastore.NewQuery("Users").Filter("Username =", data.Username) var users []User _, err = dbclient.GetAll(ctx, q, &users) if err != nil { @@ -2246,7 +2247,7 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) { if len(users) != 1 { log.Printf(`Found multiple users with the same username: %s: %d`, data.Username, len(users)) resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found multiple users with the same username: %s"}`, data.Username))) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Found %d users with the same username: %s"}`, len(users), data.Username))) return } @@ -3215,8 +3216,6 @@ 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 { @@ -6140,7 +6139,6 @@ func runInit(ctx context.Context) { } // Fix active users etc - log.Printf("Checking users") q := datastore.NewQuery("Users").Filter("active =", true) var activeusers []User _, err = dbclient.GetAll(ctx, q, &activeusers) @@ -6181,18 +6179,23 @@ func runInit(ctx context.Context) { } } } else if len(users) == 0 { - log.Printf("Trying to set up user based on environments DEFAULT_USERNAME & DEFAULT_PASSWORD") + log.Printf("Trying to set up user based on environments SHUFFLE_DEFAULT_USERNAME & SHUFFLE_DEFAULT_PASSWORD") username := os.Getenv("SHUFFLE_DEFAULT_USERNAME") password := os.Getenv("SHUFFLE_DEFAULT_PASSWORD") if len(username) == 0 || len(password) == 0 { - log.Printf("DEFAULT_USERNAME and DEFAULT_PASSWORD not defined as environments. Running without default user.") + log.Printf("SHUFFLE_DEFAULT_USERNAME and SHUFFLE_DEFAULT_PASSWORD not defined as environments. Running without default user.") } else { apikey := os.Getenv("SHUFFLE_DEFAULT_APIKEY") err = createNewUser(username, password, "admin", apikey) if err != nil { log.Printf("Failed to create default user %s: %s", username, err) + } else { + log.Printf("Successfully created user %s", username) } } + } else { + //log.Printf("Found %d users.", len(users)) + //log.Printf(users[0].Username) } } @@ -6306,7 +6309,7 @@ func runInit(ctx context.Context) { } _, err = git.Clone(storer, fs, cloneOptions) if err != nil { - log.Printf("Failed loading repo %s into memory: %s", err) + log.Printf("Failed loading repo %s into memory: %s", apis, err) } else { log.Printf("Finished git clone. Looking for updates to the repo.") dir, err := fs.ReadDir("") @@ -6354,6 +6357,7 @@ func init() { r.HandleFunc("/api/v1/users/getsettings", handleSettings).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/updateuser", handleUpdateUser).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/users/{user}", deleteUser).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/users/passwordchange", handlePasswordChange).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users", handleGetUsers).Methods("GET", "OPTIONS") // General - duplicates and old. diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index b7541c87..dd651385 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1205,6 +1205,13 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { // Adds the Testing app if it's a new workflow workflowapps, err := getAllWorkflowApps(ctx) if err == nil { + // FIXME: Add real env + //q := datastore.NewQuery("Environments").Limit(1) + //count, err := dbclient.Get(ctx, q) + //envName := "Shuffle" + //if err == nil { + //} + for _, item := range workflowapps { if item.Name == "Testing" && item.AppVersion == "1.0.0" { nodeId := "40447f30-fa44-4a4f-a133-4ee710368737" @@ -1627,10 +1634,10 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // Has to NOT be generated - //if app.Name == action.AppName && app.AppVersion == action.AppVersion { - // curapp = app - // break - //} + if app.Name == action.AppName && app.AppVersion == action.AppVersion { + curapp = app + break + } } // Check to see if the whole app is valid @@ -1985,6 +1992,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } // FIXME - this should have "execution_argument" from executeWorkflow frontend + log.Printf("EXEC: %#v", execution) if len(execution.ExecutionArgument) > 0 { workflowExecution.ExecutionArgument = execution.ExecutionArgument } diff --git a/docker-compose.yml b/docker-compose.yml index 25c4f51c..c13e60fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: depends_on: - backend backend: - #build: ./backend + build: ./backend image: frikky/shuffle:backend container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} @@ -27,10 +27,10 @@ services: - /var/run/docker.sock:/var/run/docker.sock - ${APP_HOTLOAD_LOCATION}:/shuffle-apps environment: - - ORG_ID=${ORG_ID} - DATASTORE_EMULATOR_HOST=shuffle-database:8000 - - APP_DOWNLOAD_LOCATION=${APP_DOWNLOAD_LOCATION} - APP_HOTLOAD_FOLDER=/shuffle-apps + - ORG_ID=${ORG_ID} + - APP_DOWNLOAD_LOCATION=${APP_DOWNLOAD_LOCATION} - SHUFFLE_DEFAULT_USERNAME=${SHUFFLE_DEFAULT_USERNAME} - SHUFFLE_DEFAULT_PASSWORD=${SHUFFLE_DEFAULT_PASSWORD} - SHUFFLE_DEFAULT_APIKEY=${SHUFFLE_DEFAULT_APIKEY} diff --git a/frontend/src/Admin.js b/frontend/src/Admin.js index 2c76392b..a4762fc4 100644 --- a/frontend/src/Admin.js +++ b/frontend/src/Admin.js @@ -94,7 +94,7 @@ const Admin = (props) => { }), ) .catch(error => { - alert.error("Err: ", error.toString()) + alert.error("Err: "+error.toString()) }); } diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index 87670cd2..d93aa1e4 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -792,11 +792,12 @@ const AngularWorkflow = (props) => { setSelectedEdgeIndex(workflow.branches.findIndex(data => data.id === event.target.data()["id"])) setSelectedEdge(event.target.data()) - setSelectedAction({}) - setSelectedTrigger({}) } else { //alert.info("Can't edit branches from triggers") } + + setSelectedAction({}) + setSelectedTrigger({}) } const onNodeSelect = (event) => { @@ -862,6 +863,7 @@ const AngularWorkflow = (props) => { found = true break } else if (workflow.branches[key].destination_id === edge.target && workflow.branches[key].source_id === edge.source) { + console.log(edge) alert.error("That branch already exists") event.target.remove() found = true @@ -1048,7 +1050,7 @@ const AngularWorkflow = (props) => { event.preventDefault() cy.fit(null, 50) } - break; + break; case 65: // As a poweruser myself, I found myself hitting this a few // too many times to just edit text. Need a better bind @@ -2485,13 +2487,13 @@ const AngularWorkflow = (props) => { }} style={{backgroundColor: inputColor, color: "white", height: "50px"}} > - {workflow.workflow_variables !== null ? workflow.workflow_variables.map(data => ( + {workflow.workflow_variables !== undefined && workflow.workflow_variables !== null ? workflow.workflow_variables.map(data => ( {data.name} )) : null} - {workflow.execution_variables !== null ? workflow.execution_variables.map(data => ( + {workflow.execution_variables !== undefined && workflow.execution_variables !== null ? workflow.execution_variables.map(data => ( {data.name} @@ -2649,7 +2651,7 @@ const AngularWorkflow = (props) => { placeholder={selectedAction.label} onChange={selectedNameChange} /> - {environments !== undefined && environments !== null && environments.length > 1 ? + {environments !== undefined && environments !== null && environments.length > 0 ?
Environment { style={{backgroundColor: surfaceColor, color: "white", height: "50px"}} > {parents.map(data => ( - + {data.label} ))} @@ -2439,6 +2435,8 @@ const AngularWorkflow = (props) => { InputProps={{ style:{ color: "white", + marginLeft: "5px", + maxWidth: "95%", height: "50px", fontSize: "1em", }, @@ -2470,11 +2468,6 @@ const AngularWorkflow = (props) => { // FIXME - this is a shitty solution that needs re-renders all the time datafield = { Set execution variable (optional) { - changeActionVariable(e.target.value, data.value) - }} - style={{backgroundColor: inputColor, color: "white", height: "50px"}} - > - {parents.map(data => ( - - {data.label} - - ))} - SelectDisplayProps={{ - style: { - marginLeft: 10, - } - }} - - { - changeActionVariable(data.action_field, e.target.value) - }} - />
- - } else if (data.variant === "WORKFLOW_VARIABLE") { - varcolor = "#f85a3e" - if (workflow.workflow_variables === null || workflow.workflow_variables === undefined || workflow.workflow_variables.length === 0) { - setCurrentView(2) - datafield = -
-
- Looks like you don't have any variables yet. -
-
- -
-
- } else { - // FIXME - this is a shitty solution that needs re-renders all the time - datafield = - - } - - } - const changeActionVariable = (variable, value) => { // set the name data.value = value @@ -3123,47 +3006,30 @@ const AngularWorkflow = (props) => { } return ( -
-
-
-
- {data.name} -
- -
{ - e.preventDefault() - changeActionParameterVariant("STATIC_VALUE") - }}> - +
+
+
+
+ {data.name}
- -  |  - -
{ - e.preventDefault() - changeActionParameterVariant("ACTION_RESULT") - }}> - -
-
-  |  - -
{ - e.preventDefault() - changeActionParameterVariant("WORKFLOW_VARIABLE") - }}> - -
-
-
- {datafield} -
+ +
{ + e.preventDefault() + changeActionParameterVariant("STATIC_VALUE") + }}> + +
+
+
+ {datafield} +
) } const menuItemStyle = { color: "white", + backgroundColor: inputColor } const conditionsModal = @@ -3243,6 +3109,16 @@ const AngularWorkflow = (props) => { setConditionValue(conditionValue) setVariableAnchorEl(null) }} key={"matches regex"}>matches regex + { + conditionValue.value = "larger than" + setConditionValue(conditionValue) + setVariableAnchorEl(null) + }} key={"larger than"}>larger than + { + conditionValue.value = "less than" + setConditionValue(conditionValue) + setVariableAnchorEl(null) + }} key={"less than"}>less than
@@ -3303,8 +3179,8 @@ const AngularWorkflow = (props) => { } const paperVariableStyle = { - minHeight: "50px", - maxHeight: "50px", + minHeight: 75, + maxHeight: 75, minWidth: "100%", maxWidth: "100%", marginTop: "5px", @@ -3330,15 +3206,15 @@ const AngularWorkflow = (props) => { setDestinationValue(condition.destination) setConditionsModalOpen(true) }}> -
+
{condition.source.value}
-
{}}> +
{}}> {condition.condition.value}
-
+
{condition.destination.value}
@@ -3348,6 +3224,7 @@ const AngularWorkflow = (props) => { aria-controls="long-menu" aria-haspopup="true" onClick={menuClick} + style={{color: "white",}} > @@ -3366,7 +3243,7 @@ const AngularWorkflow = (props) => { setAnchorEl(null) }} > - { + { setOpen(false) deleteCondition(index) }} key={"Delete"}>{"Delete"} @@ -3557,12 +3434,6 @@ const AngularWorkflow = (props) => { rows="10" value={selectedTrigger.parameters[0].value.split(splitter)} style={{backgroundColor: inputColor, color: "white"}} - PaperProps={{ - style: { - height: "200px", - backgroundColor: inputColor, - } - }} SelectDisplayProps={{ style: { marginLeft: 10, diff --git a/frontend/src/App.js b/frontend/src/App.js index 8eb7f108..c162b774 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -59,7 +59,14 @@ const theme = createMuiTheme({ }, typography: { useNextVariants: true - } + }, + overrides: { + MuiMenu: { + list: { + backgroundColor: inputColor, + }, + }, + }, }); diff --git a/frontend/src/Apps.js b/frontend/src/Apps.js index 8bdb587f..3e48b8e6 100644 --- a/frontend/src/Apps.js +++ b/frontend/src/Apps.js @@ -448,6 +448,12 @@ const Apps = (props) => { })}
: null} + {selectedAction.description !== undefined && selectedAction.description !== null ? +
+ Action Description
+ {selectedAction.description} +
+ : null}
: null diff --git a/frontend/src/Workflows.js b/frontend/src/Workflows.js index 043b35c7..95c2e2ba 100644 --- a/frontend/src/Workflows.js +++ b/frontend/src/Workflows.js @@ -383,21 +383,21 @@ const Workflows = (props) => { }} > - { + { setModalOpen(true) setEditingWorkflow(data) setNewWorkflowName(data.name) setNewWorkflowDescription(data.description) }} key={"change"}>{"Change name"} - { + { copyWorkflow(data) setOpen(false) }} key={"copy"}>{"Copy"} - { + { exportWorkflow(data) setOpen(false) }} key={"export"}>{"Export"} - { + { deleteWorkflow(data.id) setOpen(false) }} key={"delete"}>{"Delete"} From d02bffa7895c0e52f1d4e7cb041a76870a6ebeea Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 26 Jun 2020 08:02:27 +0200 Subject: [PATCH 09/14] Made webhooks able to connect to any node --- backend/go-app/main.go | 52 +++++++++++++++------------------ backend/go-app/walkoff.go | 11 ++++++- backend/tests/triggers.sh | 4 ++- frontend/src/AngularWorkflow.js | 21 +++++++++---- 4 files changed, 53 insertions(+), 35 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index f36fa593..1481ef6c 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -362,6 +362,7 @@ type HookAction struct { type Hook struct { Id string `json:"id" datastore:"id"` + Start string `json:"start" datastore:"start"` Info Info `json:"info" datastore:"info"` Actions []HookAction `json:"actions" datastore:"actions"` Type string `json:"type" datastore:"type"` @@ -3064,7 +3065,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { hookId = hookId[8:len(hookId)] - log.Printf("HookID: %s", hookId) + //log.Printf("HookID: %s", hookId) hook, err := getHook(ctx, hookId) if err != nil { log.Printf("Failed getting hook: %s", err) @@ -3097,6 +3098,24 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { ID: "", } + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("Body data error: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + bodyWrapper := fmt.Sprintf(`{"start": "%s", "execution_argument": "%s"}`, hook.Start, string(body)) + if len(hook.Start) == 0 { + bodyWrapper = string(body) + } + + request := &http.Request{ + Method: "POST", + Body: ioutil.NopCloser(strings.NewReader(bodyWrapper)), + } + workflowExecution, executionResp, err := handleExecution(item, workflow, request) if err == nil { @@ -3136,6 +3155,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Id string `json:"id"` Name string `json:"name"` Workflow string `json:"workflow"` + Start string `json:"start"` } body, err := ioutil.ReadAll(request.Body) @@ -3146,7 +3166,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { return } - log.Println("Data: %s", string(body)) + log.Printf("Data: %s", string(body)) ctx := context.Background() var requestdata requestData @@ -3196,6 +3216,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { hook := Hook{ Id: newId, + Start: requestdata.Start, Workflows: []string{requestdata.Workflow}, Info: Info{ Name: requestdata.Name, @@ -3216,31 +3237,6 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Running: false, } - // FIXME: Add cloud function execution? - //b, err := json.Marshal(hook) - //if err != nil { - // log.Printf("Failed marshalling: %s", err) - // resp.WriteHeader(401) - // resp.Write([]byte(`{"success": false}`)) - // return - //} - - //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 - //} - hook.Status = "running" hook.Running = true err = setHook(ctx, hook) @@ -3256,7 +3252,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { log.Printf("Failed to increase total workflows: %s", err) } - log.Println("Generating new hook") + log.Println("Set up a new hook") resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) } diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index dd651385..5ed4d784 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2015,7 +2015,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } } else if len(execution.Start) > 0 { - log.Printf("START ACTION %s IS WRONG ID LENGTH %d!", len(execution.Start)) + log.Printf("START ACTION %s IS WRONG ID LENGTH %d!", execution.Start, len(execution.Start)) return WorkflowExecution{}, fmt.Sprintf("Startnode %s was not found in actions", execution.Start), errors.New(fmt.Sprintf("Startnode %s was not found in actions", execution.Start)) } @@ -2166,11 +2166,15 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf childNodes := findChildNodes(workflowExecution, workflowExecution.Start) topic := "workflows" + startFound := false // FIXME - remove this? newActions := []Action{} defaultResults := []ActionResult{} for _, action := range workflowExecution.Workflow.Actions { action.LargeImage = "" + if action.ID == workflowExecution.Start { + startFound = true + } //log.Println(action.Environment) if action.Environment == "" { @@ -2210,6 +2214,11 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } } + if !startFound { + log.Printf("Startnode %s doesn't exist!", workflowExecution.Start) + return WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf("Workflow start node %s doesn't exist. Exiting!", workflowExecution.Start)) + } + // Verification for execution environments workflowExecution.Results = defaultResults workflowExecution.Workflow.Actions = newActions diff --git a/backend/tests/triggers.sh b/backend/tests/triggers.sh index 75d3d436..393a3f05 100644 --- a/backend/tests/triggers.sh +++ b/backend/tests/triggers.sh @@ -1 +1,3 @@ -curl -H "Content-Type: application/json" localhost:5001/api/v1/triggers/9e845679-5843-4959-a76c-a6d664e9df35 -H "Authorization: Bearer 377469e8-dd5d-4521-8d9e-416d8d2f6fd4" +# curl -H "Content-Type: application/json" localhost:5001/api/v1/triggers/9e845679-5843-4959-a76c-a6d664e9df35 -H "Authorization: Bearer 377469e8-dd5d-4521-8d9e-416d8d2f6fd4" + +curl -XPOST http://localhost:5001/api/v1/hooks/webhook_1b968d49-2d78-4132-bf91-0f3a1f6a79f3 -d '{}' diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index b5d4a71e..d34a7f28 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -1840,9 +1840,9 @@ const AngularWorkflow = (props) => { position: newposition, } - if (data.trigger_type === "WEBHOOK") { - newAppData.status = "running" - } + //if (data.trigger_type === "WEBHOOK") { + // newAppData.status = "running" + //} // Can all the data be in here? hmm const nodeToBeAdded = { @@ -1888,8 +1888,8 @@ const AngularWorkflow = (props) => { setWorkflow(workflow) if (data.trigger_type === "WEBHOOK") { - newWebhook(newAppData) - saveWorkflow(workflow) + //newWebhook(newAppData) + //saveWorkflow(workflow) } } } @@ -3837,13 +3837,24 @@ const AngularWorkflow = (props) => { return } + // Check the node it's connected to + var startNode = workflow.start + const branch = workflow.branches.find(branch => branch.source_id === trigger.id) + if (branch === undefined && (workflow.start === undefined || workflow.start === null || workflow.start.length === 0)) { + alert.error("No webhook node defined") + } + alert.info("Starting webhook") + if (branch !== undefined) { + startNode = branch.destination_id + } const data = { "name": hookname, "type": "webhook", "id": trigger.id, "workflow": workflow.id, + "start": startNode, } fetch(globalUrl+"/api/v1/hooks/new", { From 73c59a074ea3c9351da3fc83ee154c6813957e34 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 26 Jun 2020 11:51:51 +0200 Subject: [PATCH 10/14] #77: Added execution image to base --- backend/go-app/main.go | 25 +++-- backend/go-app/walkoff.go | 21 +++- frontend/src/AngularWorkflow.js | 140 +++++++++++++++++++++----- frontend/src/defaultCytoscapeStyle.js | 1 + 4 files changed, 149 insertions(+), 38 deletions(-) diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 1481ef6c..8ff15f1f 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -3074,26 +3074,28 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("HOOK FOUND: %#v", hook) + //log.Printf("HOOK FOUND: %#v", hook) // Execute the workflow //executeWorkflow(resp, request) //resp.WriteHeader(200) //resp.Write([]byte(`{"success": true}`)) if hook.Status == "stopped" { + log.Printf("Not running because hook status is stopped") resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The webhook isn't running. Click start to start it"}`))) return } if len(hook.Workflows) == 0 { + log.Printf("Not running because hook isn't connected to any workflows") 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) + log.Printf("Running for workflow %s with startnode %s", item, hook.Start) workflow := Workflow{ ID: "", } @@ -3106,17 +3108,26 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { return } - bodyWrapper := fmt.Sprintf(`{"start": "%s", "execution_argument": "%s"}`, hook.Start, string(body)) - if len(hook.Start) == 0 { - bodyWrapper = string(body) + parsedBody := string(body) + parsedBody = strings.Replace(parsedBody, "\"", "\\\"", -1) + if len(parsedBody) > 0 { + if string(parsedBody[0]) == `"` && string(parsedBody[len(parsedBody)-1]) == "\"" { + parsedBody = parsedBody[1 : len(parsedBody)-1] + } } - request := &http.Request{ + bodyWrapper := fmt.Sprintf(`{"start": "%s", "execution_source": "webhook", "execution_argument": "%s"}`, hook.Start, string(parsedBody)) + if len(hook.Start) == 0 { + log.Printf("No start node for hook %s - running with workflow default.", hook.Id) + bodyWrapper = string(parsedBody) + } + + newRequest := &http.Request{ Method: "POST", Body: ioutil.NopCloser(strings.NewReader(bodyWrapper)), } - workflowExecution, executionResp, err := handleExecution(item, workflow, request) + workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest) if err == nil { err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1) diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 5ed4d784..e05ff1df 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -61,6 +61,7 @@ var shuffleTestPath = "./shuffle-test-258209-5a2e8d7e508a.json" type ExecutionRequest struct { ExecutionId string `json:"execution_id"` ExecutionArgument string `json:"execution_argument"` + ExecutionSource string `json:"execution_source"` WorkflowId string `json:"workflow_id"` Environments []string `json:"environments"` Authorization string `json:"authorization"` @@ -153,6 +154,7 @@ type WorkflowExecution struct { Start string `json:"start" datastore:"start"` ExecutionArgument string `json:"execution_argument" datastore:"execution_argument"` ExecutionId string `json:"execution_id" datastore:"execution_id"` + ExecutionSource string `json:"execution_source" datastore:"execution_source"` WorkflowId string `json:"workflow_id" datastore:"workflow_id"` LastNode string `json:"last_node" datastore:"last_node"` Authorization string `json:"authorization" datastore:"authorization"` @@ -449,9 +451,9 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode // FIXME: // This may run multiple places if multiple servers, // but that's a future problem - log.Printf("BODY: %s", string(body)) + //log.Printf("BODY: %s", string(body)) parsedArgument := strings.Replace(string(body), "\"", "\\\"", -1) - bodyWrapper := fmt.Sprintf(`{"start": "%s", "execution_argument": "%s"}`, startNode, parsedArgument) + bodyWrapper := fmt.Sprintf(`{"start": "%s", "execution_source": "schedule", "execution_argument": "%s"}`, startNode, parsedArgument) log.Printf("WRAPPER BODY: \n%s", bodyWrapper) job := func() { request := &http.Request{ @@ -1983,7 +1985,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf var execution ExecutionRequest err = json.Unmarshal(body, &execution) if err != nil { - //log.Printf("Failed execution POST unmarshaling - still continue: %s", err) + log.Printf("Failed execution POST unmarshaling - continuing anyway: %s", err) //return WorkflowExecution{}, "", err } @@ -1992,11 +1994,15 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } // FIXME - this should have "execution_argument" from executeWorkflow frontend - log.Printf("EXEC: %#v", execution) + //log.Printf("EXEC: %#v", execution) if len(execution.ExecutionArgument) > 0 { workflowExecution.ExecutionArgument = execution.ExecutionArgument } + if len(execution.ExecutionSource) > 0 { + workflowExecution.ExecutionSource = execution.ExecutionSource + } + //log.Printf("Execution data: %#v", execution) if len(execution.Start) == 36 { log.Printf("SHOULD START ON NODE %s", execution.Start) @@ -2144,6 +2150,13 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf workflowExecution.Status = "EXECUTING" } + if len(workflowExecution.ExecutionSource) == 0 { + log.Printf("No execution source specified. Setting to default") + workflowExecution.ExecutionSource = "default" + } else { + log.Printf("Execution source is %s for execution ID %s", workflowExecution.ExecutionSource, workflowExecution.ExecutionId) + } + workflowExecution.ExecutionVariables = workflow.ExecutionVariables // Local authorization for this single workflow used in workers. diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index d34a7f28..45814704 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -31,6 +31,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel'; import Checkbox from '@material-ui/core/Checkbox'; import Breadcrumbs from '@material-ui/core/Breadcrumbs'; import CircularProgress from '@material-ui/core/CircularProgress'; +import Switch from '@material-ui/core/Switch'; import ReactJson from 'react-json-view' import { useBeforeunload } from 'react-beforeunload'; @@ -48,6 +49,7 @@ import DeleteIcon from '@material-ui/icons/Delete'; import SaveIcon from '@material-ui/icons/Save'; import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; import * as cytoscape from 'cytoscape'; import * as edgehandles from 'cytoscape-edgehandles'; @@ -123,6 +125,7 @@ const AngularWorkflow = (props) => { const [localFirstrequest, setLocalFirstrequest] = React.useState(true) const [requiresAuthentication, setRequiresAuthentication] = React.useState(true) const [rightSideBarOpen, setRightSideBarOpen] = React.useState(false) + const [showSkippedActions, setShowSkippedActions] = React.useState(false) const [variableAnchorEl, setVariableAnchorEl] = React.useState(null) @@ -803,7 +806,9 @@ const AngularWorkflow = (props) => { const onNodeSelect = (event) => { const data = event.target.data() setLastSaved(false) - console.log(data) + const branch = workflow.branches.filter(branch => branch.source_id === data.id || branch.destination_id === data.id) + console.log("NODE: ", data) + console.log("BRANCHES: ", branch) if (data.type === "ACTION") { // FIXME - unselect @@ -1677,16 +1682,7 @@ const AngularWorkflow = (props) => { ) } - const TriggersView = () => { - const triggersViewStyle = { - marginLeft: "10px", - marginRight: "10px", - display: "flex", - flexDirection: "column", - } - - // Predefined hurr - var triggers = [{ + var triggers = [{ "name": "Webhook", "type": "TRIGGER", "status": "uninitialized", @@ -1695,7 +1691,7 @@ const AngularWorkflow = (props) => { "large_image": 'data:image/png;base64,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', "is_valid": true, "label": "Webhook", - "environment": environments[0].Name, + "environment": environments[0] === undefined ? {} : environments[0].Name, "description": "Simple HTTP webhook", "long_description": "Execute a workflow with an unauthicated POST request", }, @@ -1709,7 +1705,7 @@ const AngularWorkflow = (props) => { "errors": null, "is_valid": false, "label": "User input", - "environment": environments[0].Name, + "environment": environments[0] === undefined ? {} : environments[0].Name, "long_description": "Take user input to continue execution", }, { @@ -1722,7 +1718,7 @@ const AngularWorkflow = (props) => { "large_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAAAAABVicqIAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfjCB8QNSt2pVcCAAAIxUlEQVRo3u2aa4xV1RXH/2vtfWeAYQbBBwpUBqGkjQLKw4LQ1NqmDy2RxmKJSGxiaatttbWhabQPSFuT2i+NqdFKbVqjjDZGYxqgxFbbWgHlNSkBChQj8ii+ZV4Mc/be/34459w5995zH4zYpA3709x7z9m/vdbae6/XCPH+D/0vMM5AzkDOQBoY9hSfJ4n4khCISGMvySlcKww0UvYNpAFdNAxhEAXQc/T1g2/2RFJoOW/8OeNHAaDXepwGIYEGOL5146a9/z5R/LJp7KRp86+4UOJf3yskUKVv/ZN/OQoAogIIQQYAaJ117aL2ehjWHcEF7lsxEQK1RgeNLaLGKgSti5919L76DPUhzvPAV0cAanL3khgDyMf+GOjCUCHB8Z0VI6GFGsYVq8Cnt1cXpg7Eez7ZXiKEiKgxqqqSFUcx7M5+uqFAHLuWQwYRYqzNfsjAjWLmdka5Kqu5u5ztvOkfNoTko4oHICNHtzSHqK+rywMwCEyZruX+ZV5zLFcL4uzTy7qtT24RZUDh4ivmTL2wdbgN/mTvkZd3bO58F2KKTwT+aGXIu2uq6yribxQmMYRRYMZPO8uVfmTNouGx4QFALW6OQqX5q0McV8MkR0wNdOEzAyRd5H2Ih3cuMHD3N0ZB0+ea8MUBhoYhER9GcimJUXz0eTJEvvx9H/nAA19pQrwFRApY6iueqgZxXF9ItCsWZz0Y6KocNu8Ct8xBqjKL2+kbg3juH5vao4D5/6SrcgRiDPu/J4nYanF/+XnJhwT2z0v8mRjc0l/jyojldvz9qHhRotL81zJKPsTxjoShBj+uefklq4q4KRXd4NKeUuMjn7E21ZXFPVWOcdkY4PaxScRgcUepKHmQwJ5Liqta1RiDjLi5LaaImL+VUPIgjj+LlSUWt1Saw3vvK7cpGbEjsb7BfJdVWA4k8Nj5UAHEYt5JNiZHTFmZWNLgt1lRciCOK2FFAEHLjvLdGHhi+eev/8J112ytOA0MwX08VrPi0uzBr4QEvj4BEhvwbkYVv3adC4HiDznOw3P7SKgAMOjI/F7p8AI6DlsCUDf9NlTGB9oMaw3yAgd1l92exqSrs8FppSBuNgwAUTxeudrA7gugUKzNc4OBr10YvyzmpUF9VkhCbN4qAYCGuYvz1pveaHkeSPx5X4MAoPonUHRVOZCnYeOfbxWfM1F/BIJVInXFstFOABDrnWEVCI1/BgGA+kmfy5mJ6Pf5q0tEmXAtFACxcweqQrBnFwIAwWdH+0qdCOqF8tcjAKDBCwhVIS9GhgCIhVVmqRnYKha0J4Z+oWi3Sqm3xSsO4y8fSoYkvnV+bHp09qVGKZuHBjuT72eOCQ3mOGVjbiLvkWPIhwA9h0EAgukYYtllNrwA1BP7qkCI195EbJIZQ4MIJoyGAFAcqirJWz0ggICx+ecNI5sACFxVyLjk6sOR9Dsbrx+gAEDQ4xACQnsWSEr65uAwAmH1PSbUs5M/j6bv2XSO9LLoSyLXEaOgjWa3pRqXtuSv3hLIu7CuRwHAjetKfjFvjxgQFlrAUOgbMbxyruqYpmSKgYy6gm5dYk2/gBA2DcADII5/UgBqE2GOT3tqOCuFCrWz6+wqSHr+LqP28tkUk3YP3tqBPRdAIZj5HENuNOa5EAaAxQ3pa4gd7mNGraqKDKYXoiKipoCp+zOeNrB7AgQQmGUH6XN9ypUJZHnqcpCEAB2an3gWcNG+rHsKfOccWADG4Oyf91XGfYH+kgTyw9R5Iw001qjmeCiDSbvLXGC4pxWqcTo6fV2FzjwPnYXYzT9YBmHEx4ya8j1r0b67Ml751xKBUYEayOL99CUYx01ITvz6UnWlGtPSjK+Ai/ZUunIXuGE21ApgDNpW9ZbozPGXsZfH8AMlhk8ppnRTWkzZmxcueMeT950LNRCxig894TM7w/FGGACKD/aloVcmWonYYTIFH7GYvK9KZu48jy63MCqiRnDN7mIkF9jVDgVgcF3x5WxIVLrHCphSjRHXWzYuiFNSNWi+N5XFcV186Vr8ohgZlsRdA+wwYlJdTd7L2ulVeGgcjAGkGb9KH3W8KTGJbCkqsTS4i7hGjYEILCbVZCQ6+3oTjLH41ODWezX1JtOiog7LIsiIHUaNqEX7rjoMMjjP7VdBTWFTuuiId8PGBv3u4PvlYWpsfYuJ9Rmxzvyjk/Ht9NnAYx+AojxMrYiFI3YYg8m7G2GQdI5v/eRQqpiId8IKAItP1EwdIj5e3x5ZnYXidJ7bW9KsY01mhpwkKOKvX2qYQTKkSWUI0VVpEnRZ7SSI9GTdnDpvRFyVuHODh+ukcyy9vxvmRVwTuyOxWFAvMS0XyzeaYm9sjXM5Eft8ydrqQTx39IdGhBngtrGIfYXFd+oXC0qW9xDuyvWypSNE3DhY9pje1UDZI8NYrYovdderSjjHx9riEwLBsL83VMApMh6BqsWcnTWF8Y4nVkBt6iEeaKwUlbycuDGD1ntdmZfNIgI3zoLR1EN8q9GiWsx4NHFiRvGRZ8ngqpQHv9wEW4xIbwwNlwdJz4Nj0kaRGshn1p4k6SJXVujcdWsb1CYBtcWSUyl0koFrmpEWIo0CF6/aVm6Zw49cMywtgYsYi5tdzoavVXwOuuGGtwsuU3y2H55/+ZT21hE2hP7eI/s7X+w8DjFJCVyUIb/4XLOM7s3+pVuSMrqARjwBtI5qbeZAb/fxAMBISDppYtzIB5bmltHrNQR6bwNsMbQULekBZD6INZi1o8p5qt/a2DC1rD8jqqpamiEZg+HfPzG01gYZHLt/0AYxtZo0RoGrO4fcpCHpPF/5ZhugNie9E1FrAblyHd9Du4mxg335rilp4yyrN2MNBK1LnvM1a8cNtwBP/OmpP78aLz4WiHF3pm3u1Ysm1mkBnkozs3vbxk17jmabmRfNmD9vwmlqZsYLhwHQe/SNV97ojrTQcv74MRPaAIRwutqyCYdl853uBnM6LdNqxPvUKh/y+P/594UzkDOQ/3HIfwCAE6puXSx5zQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wOC0zMVQxNjo1Mzo0My0wNDowMGtSg1gAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTktMDgtMzFUMTY6NTM6NDMtMDQ6MDAaDzvkAAAAAElFTkSuQmCC", "is_valid": true, "label": "Schedule", - "environment": environments[0].Name, + "environment": environments[0] === undefined ? {} : environments[0].Name, "long_description": "Create a schedule based on cron", }, { @@ -1734,11 +1730,21 @@ const AngularWorkflow = (props) => { "errors": null, "is_valid": false, "label": "Email", - "environment": environments[0].Name, + "environment": environments[0] === undefined ? {} : environments[0].Name, "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", }] + const TriggersView = () => { + const triggersViewStyle = { + marginLeft: "10px", + marginRight: "10px", + display: "flex", + flexDirection: "column", + } + + // Predefined hurr + return (
@@ -2553,6 +2559,7 @@ const AngularWorkflow = (props) => { paddingRight: 10, paddingLeft: 10, minHeight: "100%", + zIndex: 1000, } const defineStartnode = () => { @@ -4418,6 +4425,67 @@ const AngularWorkflow = (props) => { maxHeight: 40, } + const parsedExecutionArgument = () => { + var showResult = executionData.execution_argument.trim() + showResult.split(" None").join(" \"None\"") + + var jsonvalid = true + try { + const tmp = String(JSON.parse(showResult)) + if (!tmp.includes("{") && !tmp.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } + + if (jsonvalid) { + return ( + + ) + } + + return ( +
+

Execution Argument:

+ {executionData.execution_argument} +
+ ) + } + + + const getExecutionSourceImage = (execution) => { + const defaultImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg==" + const size = 40 + console.log("EXECUTION SOURCE: ", execution.execution_source) + if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) { + return default + } + + if (execution.execution_source === "webhook") { + return {"webhook"} trigger.trigger_type === "WEBHOOK").large_image} style={{width: size, height: size}} /> + } + + if (execution.execution_source === "schedule") { + return {"schedule"} trigger.trigger_type === "SCHEDULE").large_image} style={{width: size, height: size}} /> + } + + if (execution.execution_source === "EMAIL") { + return {"email"} trigger.trigger_type === "EMAIL").large_image} style={{width: size, height: size}} /> + } + + + return ( + {execution.execution_source} + ) + } + const executionModal = setExecutionModalOpen(false)} PaperProps={{style: {minWidth: 375, maxWidth: 375, backgroundColor: "#1F2023", color: "white", fontSize: 18}}}> {executionModalView === 0 ? @@ -4446,13 +4514,16 @@ const AngularWorkflow = (props) => { const timestamp = new Date(data.started_at*1000).toISOString().split('.')[0].split("T").join(" ") return ( - {}} onMouseOut={() => {}} onClick={() => { + {}} onMouseOut={() => {}} onClick={() => { setExecutionModalView(1) setExecutionData(data) }}>
-
-
+
+
+ {getExecutionSourceImage(data)} +
+
{timestamp}
{data.workflow.actions !== null ? @@ -4481,17 +4552,19 @@ const AngularWorkflow = (props) => {

{setExecutionModalView(0)}}> - - Other Executions + + See other Executions

Executing Workflow

- {executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ? -
-

Execution Argument:

{executionData.execution_argument} -
- : null } + Show skipped actions
+ control={ + {setShowSkippedActions(!showSkippedActions)}} /> + } + /> {executionData.status !== undefined && executionData.status.length > 0 ?
Status: {executionData.status} @@ -4504,12 +4577,21 @@ const AngularWorkflow = (props) => {
: null } - {executionData.execution_id !== undefined && executionData.execution_id.length > 0 ? + {executionData.completed_at !== undefined ?
- ID: {executionData.execution_id} + Finished: {new Date(executionData.completed_at*1000).toISOString()}
: null } + {executionData.execution_source !== undefined && executionData.execution_source.length > 0 ? +
+ Source: {executionData.execution_source} +
+ : null + } + {executionData.execution_argument !== undefined && executionData.execution_argument.length > 0 ? + parsedExecutionArgument() + : null }
Actions @@ -4521,6 +4603,10 @@ const AngularWorkflow = (props) => { : executionData.results.map(data => { + if (!showSkippedActions && data.status === "SKIPPED") { + return null + } + var showResult = data.result.trim() showResult.split(" None").join(" \"None\"") diff --git a/frontend/src/defaultCytoscapeStyle.js b/frontend/src/defaultCytoscapeStyle.js index 4ad4f369..d101c1cc 100644 --- a/frontend/src/defaultCytoscapeStyle.js +++ b/frontend/src/defaultCytoscapeStyle.js @@ -24,6 +24,7 @@ const data = [{ 'curve-style': 'unbundled-bezier', 'label': 'data(label)', 'text-margin-y': '-15px', + "color": "white", "line-fill": "linear-gradient", "line-gradient-stop-colors": ["cyan", "yellow"], "line-gradient-stop-positions": ["0.0", "100"], From 6a8707332078fa9fdba7699d56b437077cd7a578 Mon Sep 17 00:00:00 2001 From: frikky Date: Fri, 26 Jun 2020 13:30:04 +0200 Subject: [PATCH 11/14] #71: Added control options to the workflow --- backend/app_sdk/app_base.py | 7 ++-- backend/go-app/walkoff.go | 3 +- frontend/src/AngularWorkflow.js | 69 +++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index cd7fc992..f607e452 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -428,7 +428,7 @@ class AppBase: except KeyError as e: print("Lower keyerror: %s" % e) - return "KeyError: %s" % e + return "KeyError: Couldn't find key: %s" % e return basejson @@ -795,7 +795,7 @@ class AppBase: result += "Failed autocasting. Can't handle %s type from function. Must be string" % type(newres) print("Can't handle type %s value from function" % (type(newres))) else: - print("APP_SDK DONE: Starting multi execution with", multi_parameters) + print("APP_SDK DONE: Starting MULTI execution with", multi_parameters) # 1. Use number of executions based on longest array # 2. Find the right value from the parsed multi_params @@ -848,10 +848,11 @@ class AppBase: self.logger.debug(f"Executed {action['label']}-{action['id']} with result: {result}") self.logger.debug(f"Data: %s" % action_result) except TypeError as e: + print("TypeError issue: %s" % e) action_result["status"] = "FAILURE" action_result["result"] = "TypeError: %s" % str(e) else: - print("Not callable?") + print("Function %s doesn't exist?" % action["name"]) self.logger.error(f"App {self.__class__.__name__}.{action['name']} is not callable") action_result["status"] = "FAILURE" action_result["result"] = "Function %s is not callable." % actionname diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index e05ff1df..5aa2aa45 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1250,7 +1250,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Actions = newActions workflow.IsValid = true - workflow.Configuration.ExitOnError = true + workflow.Configuration.ExitOnError = false workflowjson, err := json.Marshal(workflow) if err != nil { @@ -1573,6 +1573,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // doesn't check sharing=true // Have to do it like this to add the user's apps log.Println("Apps set starting") + log.Printf("EXIT ON ERROR: %#v", workflow.Configuration.ExitOnError) workflowApps := []WorkflowApp{} //memcacheName = "all_apps" //if item, err := memcache.Get(ctx, memcacheName); err == memcache.ErrCacheMiss { diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index 45814704..db735647 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -50,6 +50,7 @@ import SaveIcon from '@material-ui/icons/Save'; import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight'; import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import SettingsIcon from '@material-ui/icons/Settings'; import * as cytoscape from 'cytoscape'; import * as edgehandles from 'cytoscape-edgehandles'; @@ -4202,7 +4203,7 @@ const AngularWorkflow = (props) => { return null } - const cytoscapeViewWidths = 650 + const cytoscapeViewWidths = 700 const bottomBarStyle = { position: "fixed", right: 20, @@ -4260,8 +4261,67 @@ const AngularWorkflow = (props) => { return null } + const WorkflowMenu = () => { + const [newAnchor, setNewAnchor] = React.useState(null); + const [showShuffleMenu, setShowShuffleMenu] = React.useState(false) + + { /*const [showShuffleMenu, setShowShuffleMenu] = React.useState(true) */} + return ( +
+ { + setShowShuffleMenu(false) + }} + > +
+

This menu is used to control the workflow itself.

+ + Exit on Error
+ control={ + { + workflow.configuration.exit_on_error = !workflow.configuration.exit_on_error + setWorkflow(workflow) + setUpdate("exit_on_error_"+workflow.configuration.exit_on_error ? "true" : "false") + setShowShuffleMenu(false) + }} /> + } + /> + Start from top
+ control={ + { + workflow.configuration.start_from_top = !workflow.configuration.start_from_top + setWorkflow(workflow) + setUpdate("start_from_top_"+workflow.configuration.start_from_top ? "true" : "false") + setShowShuffleMenu(false) + }} /> + } + /> +
+ + + + +
+ ) + } const BottomCytoscapeBar = () => { + const [anchorEl, setAnchorEl] = React.useState(null); + const boxSize = 100 const executionButton = executionRunning ? @@ -4328,6 +4388,7 @@ const AngularWorkflow = (props) => { +
) @@ -4461,9 +4522,9 @@ const AngularWorkflow = (props) => { const getExecutionSourceImage = (execution) => { + // This is the playbutton at 150x150 const defaultImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg==" const size = 40 - console.log("EXECUTION SOURCE: ", execution.execution_source) if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) { return default } @@ -4560,7 +4621,7 @@ const AngularWorkflow = (props) => {

Executing Workflow

Show skipped actions
+ label=
Show failed / skipped actions
control={ {setShowSkippedActions(!showSkippedActions)}} /> } @@ -4603,7 +4664,7 @@ const AngularWorkflow = (props) => { : executionData.results.map(data => { - if (!showSkippedActions && data.status === "SKIPPED") { + if (!showSkippedActions && (data.status === "SKIPPED" || data.status === "FAILURE")) { return null } From 366d14a3e8dbb64667e2fdd52391ea1df6eaac2b Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 27 Jun 2020 07:40:49 +0200 Subject: [PATCH 12/14] Fixed JSON parsing within string --- backend/app_sdk/app_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index f607e452..94f31d09 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -409,6 +409,9 @@ class AppBase: print("INDEXVAL: ", indexvalue) return indexvalue + except TypeError as e: + print("Inner TypeError: %s" % e) + return basejson # Example format: ${[]}$ parseditem = "${%s%s}$" % (parsersplit[cnt+1], json.dumps(returnlist)) @@ -436,7 +439,10 @@ class AppBase: def parse_params(action, fullexecution, parameter): # Skip if it starts with $? jsonparsevalue = "$." - match = ".*([$]{1}([a-zA-Z0-9# _-]+\.?){1,})" + + # Matches with space in the first part, but not in subsequent parts. + # JSON / yaml etc shouldn't have spaces in their fields anyway. + match = ".*?([$]{1}([a-zA-Z0-9 _-]+\.?){1}([a-zA-Z0-9_-]+\.?){0,})" # Regex to find all the things if parameter["variant"] == "STATIC_VALUE": From dfc639c92ba07bbd8520cc4e15bae4144be4a944 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 27 Jun 2020 08:02:29 +0200 Subject: [PATCH 13/14] Made app_sdk loop parser more stable. Still missing recursion. --- backend/app_sdk/app_base.py | 24 ++++++---- backend/tests/forparser.py | 90 ++++++++++++++++++++++++++++--------- 2 files changed, 84 insertions(+), 30 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 94f31d09..c39233a8 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -340,7 +340,7 @@ class AppBase: #print("WF Variables: %s" % execution_data["workflow"]["workflow_variables"]) for variable in execution_data["workflow"]["workflow_variables"]: variablename = variable["name"].replace(" ", "_", -1).lower() - + if variablename.lower() == actionname_lower: baseresult = variable["value"] break @@ -350,7 +350,7 @@ class AppBase: except TypeError as e: print("TypeError wf variables: %s" % e) pass - + print("BEFORE EXECUTION VAR") if len(baseresult) == 0: try: @@ -386,18 +386,20 @@ class AppBase: except json.decoder.JSONDecodeError as e: return baseresult + # This whole thing should be recursive. try: cnt = 0 for value in parsersplit[1:]: cnt += 1 + print("VALUE: %s" % value) if value == "#": # FIXME - not recursive - should go deeper if there are more # print("HANDLE RECURSIVE LOOP OF %s" % basejson) returnlist = [] try: for innervalue in basejson: - print("Value: %s" % value[parsersplit[cnt+1]]) + print("Value: %s" % innervalue[parsersplit[cnt+1]]) returnlist.append(innervalue[parsersplit[cnt+1]]) except IndexError as e: print("Indexerror inner: %s" % e) @@ -406,12 +408,11 @@ class AppBase: indexvalue = "${NO_SPLITTER%s}$" % json.dumps(basejson) if len(returnlist) > 0: indexvalue = "${NO_SPLITTER%s}$" % json.dumps(returnlist) - + print("INDEXVAL: ", indexvalue) return indexvalue except TypeError as e: - print("Inner TypeError: %s" % e) - return basejson + print("TypeError inner: %s" % e) # Example format: ${[]}$ parseditem = "${%s%s}$" % (parsersplit[cnt+1], json.dumps(returnlist)) @@ -419,6 +420,10 @@ class AppBase: return parseditem else: + print("BEFORE NORMAL VALUE: ", basejson, value) + if len(value) == 0: + return basejson + if isinstance(basejson[value], str): print(f"LOADING STRING '%s' AS JSON" % basejson[value]) try: @@ -432,8 +437,9 @@ class AppBase: except KeyError as e: print("Lower keyerror: %s" % e) return "KeyError: Couldn't find key: %s" % e - - return basejson + + return basejson + def parse_params(action, fullexecution, parameter): @@ -442,7 +448,7 @@ class AppBase: # Matches with space in the first part, but not in subsequent parts. # JSON / yaml etc shouldn't have spaces in their fields anyway. - match = ".*?([$]{1}([a-zA-Z0-9 _-]+\.?){1}([a-zA-Z0-9_-]+\.?){0,})" + match = ".*?([$]{1}([a-zA-Z0-9 _-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,})" # Regex to find all the things if parameter["variant"] == "STATIC_VALUE": diff --git a/backend/tests/forparser.py b/backend/tests/forparser.py index 2b9d7100..ec748b56 100644 --- a/backend/tests/forparser.py +++ b/backend/tests/forparser.py @@ -1,9 +1,7 @@ import re import json -fullexecution = {'type': 'workflow', 'status': 'EXECUTING', 'start': '', 'execution_argument': '', 'execution_id': 'a0a1c723-772e-4569-867e-0228ced98ace', 'workflow_id': '74ab469b-5323-41b9-96c8-4214f49185d2', 'last_node': '', 'authorization': '05205468-6b13-485b-9716-6859e0a5a2a6', 'result': '', 'started_at': 1589450462, 'completed_at': 0, 'project_id': 'shuffle', 'locations': ['europe-west2'], 'workflow': {'actions': [{'app_name': 'testing', 'app_version': '1.0.0', 'app_id': 'ab9549af-d6d5-470a-ae64-1fbe50783f05', 'errors': None, 'id': '6e74deef-4f3b-4ab9-97b1-9370e2040a6f', 'is_valid': True, 'isStartNode': True, 'sharing': True, 'private_id': '', 'label': 'testing_1', 'small_image': '', 'large_image': '', 'environment': 'Shuffle', 'name': 'repeat_back_to_me', 'parameters': [{'description': 'message to repeat', 'id': '', 'name': 'call', 'example': '', 'value': '', 'multiline': False, 'action_field': 'Execution Argument', 'variant': 'ACTION_RESULT', 'required': True, 'schema': {'type': 'string'}}], 'position': {'x': 598, 'y': 559}, 'priority': 0}, {'app_name': 'testing', 'app_version': '1.0.0', 'app_id': 'dd1bd74b-bf84-48a2-99c6-8bca8d7c9ce6', 'errors': None, 'id': '3de7817c-aeda-432f-b780-69f5549c01c7', 'is_valid': True, 'isStartNode': False, 'sharing': True, 'private_id': '', 'label': 'testing_2', 'small_image': '', 'large_image': '', 'environment': 'Shuffle', 'name': 'repeat_back_to_me', 'parameters': [{'description': 'message to repeat', 'id': '', 'name': 'call', 'example': '', 'value': '{"data": "$testing2"}', 'multiline': False, 'action_field': 'testing_2', 'variant': 'STATIC_VALUE', 'required': True, 'schema': {'type': 'string'}}], 'position': {'x': 384.125, 'y': 617.625}, 'priority': 0}, {'app_name': 'testing', 'app_version': '1.0.0', 'app_id': 'dd1bd74b-bf84-48a2-99c6-8bca8d7c9ce6', 'errors': None, 'id': '40066e27-7a9f-4bb6-89cf-cea7575694ba', 'is_valid': True, 'isStartNode': False, 'sharing': True, 'private_id': '', 'label': 'testing_3', 'small_image': '', 'large_image': '', 'environment': 'Shuffle', 'name': 'repeat_back_to_me', 'parameters': [{'description': 'message to repeat', 'id': '', 'name': 'call', 'example': '', 'value': '{"data": "$testing2"}', 'multiline': False, 'action_field': 'testing_2', 'variant': 'STATIC_VALUE', 'required': True, 'schema': {'type': 'string'}}], 'position': {'x': 166.125, 'y': 561.125}, 'priority': 0}], 'branches': [{'destination_id': '6e74deef-4f3b-4ab9-97b1-9370e2040a6f', 'id': '48bbbe8e-e49a-48db-a5ad-16c21e52dd95', 'source_id': '43e4667a-e3c5-472e-8748-80cbd4984ea8', 'label': '', 'has_errors': False, 'conditions': None}, {'destination_id': '3de7817c-aeda-432f-b780-69f5549c01c7', 'id': '8a78af67-bea8-48d1-a52d-84af5fb77d32', 'source_id': '6e74deef-4f3b-4ab9-97b1-9370e2040a6f', 'label': '', 'has_errors': False, 'conditions': None}, {'destination_id': '40066e27-7a9f-4bb6-89cf-cea7575694ba', 'id': 'ddca1983-c591-44ff-932b-617ac419ac6b', 'source_id': '3de7817c-aeda-432f-b780-69f5549c01c7', 'label': '', 'has_errors': False, 'conditions': None}], 'triggers': [{'app_name': 'Webhook', 'description': 'Simple HTTP webhook', 'long_description': 'Execute a workflow with an unauthicated POST request', 'status': 'running', 'app_version': '1.0.0', 'errors': None, 'id': '43e4667a-e3c5-472e-8748-80cbd4984ea8', 'is_valid': True, 'isStartNode': False, 'label': 'Webhook', 'small_image': '', 'large_image': 'data:image/png;base64,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', 'environment': 'Shuffle', 'trigger_type': 'WEBHOOK', 'name': 'Webhook', 'parameters': [{'description': '', 'id': '', 'name': 'url', 'example': '', 'value': 'http://192.168.3.6:5001/api/v1/hooks/webhook_43e4667a-e3c5-472e-8748-80cbd4984ea8', 'multiline': False, 'action_field': '', 'variant': '', 'required': False, 'schema': {'type': ''}}, {'description': '', 'id': '', 'name': 'tmp', 'example': '', 'value': 'webhook_43e4667a-e3c5-472e-8748-80cbd4984ea8', 'multiline': False, 'action_field': '', 'variant': '', 'required': False, 'schema': {'type': ''}}], 'position': {'x': 220.75, 'y': 375.75}, 'priority': 0}], 'schedules': None, 'id': '74ab469b-5323-41b9-96c8-4214f49185d2', 'is_valid': True, 'name': 'Webhook', 'description': '', 'start': '6e74deef-4f3b-4ab9-97b1-9370e2040a6f', 'owner': '16c6e4ff-58c3-49a2-a064-8abf0c37a108', 'sharing': 'private', 'execution_org': {'name': '', 'org': '', 'users': None, 'id': ''}, 'workflow_variables': None}, 'results': [{'action': {'app_name': 'testing', 'app_version': '1.0.0', 'app_id': '', 'errors': None, 'id': '6e74deef-4f3b-4ab9-97b1-9370e2040a6f', 'is_valid': True, 'isStartNode': True, 'sharing': False, 'private_id': '', 'label': 'testing_1', 'small_image': '', 'large_image': '', 'environment': 'Shuffle', 'name': 'repeat_back_to_me', 'parameters': [{'description': 'message to repeat', 'id': '', 'name': 'call', 'example': '', 'value': '', 'multiline': False, 'action_field': 'Execution Argument', 'variant': 'ACTION_RESULT', 'required': True, 'schema': {'type': 'string'}}], 'position': {'x': 598, 'y': 559}, 'priority': 0}, 'execution_id': 'a0a1c723-772e-4569-867e-0228ced98ace', 'authorization': '05205468-6b13-485b-9716-6859e0a5a2a6', 'result': '[ { "severity": 2, "owner": "frikky", "_routing": "6UhYGXIBGUwc2QpmoXZI", "flag": false, "customFields": {}, "_type": "case", "description": "This is a desc", "title": "Aaaaand another case here :)", "tags": [], "createdAt": 1589563137969, "_parent": null, "createdBy": "frikky", "caseId": 6, "tlp": 2, "metrics": {}, "_id": "6UhYGXIBGUwc2QpmoXZI", "id": "6UhYGXIBGUwc2QpmoXZI", "_version": 1, "pap": 2, "startDate": 1589563080000, "status": "Open" }, { "severity": 2, "owner": "frikky", "_routing": "60heGXIBGUwc2QpmgXbc", "flag": false, "customFields": {}, "_type": "case", "description": ":)", "title": "The last challenge", "tags": [], "createdAt": 1589563522665, "_parent": null, "createdBy": "frikky", "caseId": 7, "tlp": 2, "metrics": {}, "_id": "60heGXIBGUwc2QpmgXbc", "id": "60heGXIBGUwc2QpmgXbc", "_version": 1, "pap": 2, "startDate": 1589563500000, "status": "Open" }, { "severity": 2, "owner": "frikky", "_routing": "5UhLGXIBGUwc2Qpmj3YR", "flag": false, "customFields": {}, "_type": "case", "description": "Description", "title": "This is a cool title for a case, right?", "tags": [], "createdAt": 1589562281320, "_parent": null, "createdBy": "frikky", "caseId": 4, "tlp": 2, "metrics": {}, "_id": "5UhLGXIBGUwc2Qpmj3YR", "id": "5UhLGXIBGUwc2Qpmj3YR", "_version": 1, "pap": 2, "startDate": 1589562240000, "status": "Open" }, { "severity": 2, "owner": "frikky", "_routing": "50hXGXIBGUwc2QpmDnZC", "flag": false, "customFields": {}, "_type": "case", "description": "Case desc", "title": "Another case!", "tags": [], "createdAt": 1589563034722, "_parent": null, "createdBy": "frikky", "caseId": 5, "tlp": 2, "metrics": {}, "_id": "50hXGXIBGUwc2QpmDnZC", "id": "50hXGXIBGUwc2QpmDnZC", "_version": 1, "pap": 2, "startDate": 1589563020000, "status": "Open" }, { "severity": 2, "owner": "frikky", "_routing": "4UgXGXIBGUwc2QpmBXa3", "flag": false, "customFields": {}, "_type": "case", "description": "This is a webhook test", "title": "TheHive webhook test", "tags": [ "Integration", "Shuffle", "Test", "TheHive" ], "createdAt": 1589558837830, "_parent": null, "createdBy": "frikky", "caseId": 2, "tlp": 0, "metrics": {}, "_id": "4UgXGXIBGUwc2QpmBXa3", "id": "4UgXGXIBGUwc2QpmBXa3", "_version": 1, "pap": 2, "startDate": 1589558820000, "status": "Open" }, { "severity": 2, "owner": "frikky", "_routing": "40g4GXIBGUwc2Qpmy3Zn", "flag": false, "customFields": {}, "_type": "case", "description": "Case for testing webhook 2", "title": "My new case testing webhook", "tags": [], "createdAt": 1589561051232, "_parent": null, "createdBy": "frikky", "caseId": 3, "tlp": 2, "metrics": {}, "_id": "40g4GXIBGUwc2Qpmy3Zn", "id": "40g4GXIBGUwc2Qpmy3Zn", "_version": 1, "pap": 2, "startDate": 1589561040000, "status": "Open" }, { "severity": 2, "owner": "frikky", "_routing": "30gKGXIBGUwc2Qpm1nZs", "flag": false, "customFields": {}, "_type": "case", "description": "asd", "title": "testing", "tags": [], "createdAt": 1589558039989, "_parent": null, "createdBy": "frikky", "caseId": 1, "tlp": 2, "metrics": {}, "_id": "30gKGXIBGUwc2Qpm1nZs", "id": "30gKGXIBGUwc2Qpm1nZs", "_version": 1, "pap": 2, "startDate": 1589557980000, "status": "Open" } ]', 'started_at': 1589450468, 'completed_at': 1589450468, 'status': 'SUCCESS'}, {'action': {'app_name': 'testing', 'app_version': '1.0.0', 'app_id': '', 'errors': None, 'id': '3de7817c-aeda-432f-b780-69f5549c01c7', 'is_valid': True, 'isStartNode': False, 'sharing': False, 'private_id': '', 'label': 'testing_2', 'small_image': '', 'large_image': '', 'environment': 'Shuffle', 'name': 'repeat_back_to_me', 'parameters': [{'description': 'message to repeat', 'id': '', 'name': 'call', 'example': '', 'value': '{"data": "$testing2"}', 'multiline': False, 'action_field': 'testing_2', 'variant': 'STATIC_VALUE', 'required': True, 'schema': {'type': 'string'}}], 'position': {'x': 384.125, 'y': 617.625}, 'priority': 0}, 'execution_id': 'a0a1c723-772e-4569-867e-0228ced98ace', 'authorization': '05205468-6b13-485b-9716-6859e0a5a2a6', 'result': '{"data": "$testing2"}', 'started_at': 1589450479, 'completed_at': 1589450479, 'status': 'SUCCESS'}, {'action': {'app_name': 'testing', 'app_version': '1.0.0', 'app_id': '', 'errors': None, 'id': '40066e27-7a9f-4bb6-89cf-cea7575694ba', 'is_valid': True, 'isStartNode': False, 'sharing': False, 'private_id': '', 'label': 'testing_3', 'small_image': '', 'large_image': '', 'environment': 'Shuffle', 'name': 'repeat_back_to_me', 'parameters': [{'description': 'message to repeat', 'id': '', 'name': 'call', 'example': '', 'value': '{"data": "$testing2"}', 'multiline': False, 'action_field': 'testing_2', 'variant': 'STATIC_VALUE', 'required': True, 'schema': {'type': 'string'}}], 'position': {'x': 166.125, 'y': 561.125}, 'priority': 0}, 'execution_id': 'a0a1c723-772e-4569-867e-0228ced98ace', 'authorization': '05205468-6b13-485b-9716-6859e0a5a2a6', 'result': '', 'started_at': 1589450489, 'completed_at': 0, 'status': 'EXECUTING'}]} - - +fullexecution = {"type":"workflow","status":"FINISHED","start":"40447f30-fa44-4a4f-a133-4ee710368737","execution_argument":"","execution_id":"083eaa87-17ff-4aba-996c-83245051cf3d","execution_source":"default","workflow_id":"d7b73e8a-08fe-460e-987b-971cb6f1857f","last_node":"40447f30-fa44-4a4f-a133-4ee710368737","authorization":"b1264cd6-ed7a-4839-8caf-187eee8804b9","result":"TypeError: list indices must be integers or slices, not str","started_at":1593236788,"completed_at":1593236790,"project_id":"shuffle","locations":["europe-west2"],"workflow":{"actions":[{"app_name":"Testing","app_version":"1.0.0","app_id":"5411f573-9bba-44c4-a8d3-0e2bb704546d","errors":"null","id":"40447f30-fa44-4a4f-a133-4ee710368737","is_valid":True,"isStartNode":True,"sharing":True,"private_id":"","label":"Hello this is a name","small_image":"","large_image":"","environment":"Shuffle","name":"repeat_back_to_me","parameters":[{"description":"The message to repeat","id":"","name":"call","example":"REPEATING: Hello world","value":"$this is a test.name is not the same as $this is a test.name2 \n\n\nNot list $this is a test.loop.# either","multiline":True,"action_field":"","variant":"STATIC_VALUE","required":True,"schema":{"type":"string"}}],"execution_variable":{"description":"","id":"","name":"","value":""},"position":{"x":360.5,"y":454.5},"priority":0}],"branches":"null","triggers":"null","schedules":"null","configuration":{"exit_on_error":False,"start_from_top":False},"id":"d7b73e8a-08fe-460e-987b-971cb6f1857f","is_valid":True,"name":"App sdk parser testing","description":"","start":"40447f30-fa44-4a4f-a133-4ee710368737","owner":"43c36230-0a6e-40fc-aebc-a8ef57c81a88","sharing":"private","execution_org":{"name":"","org":"","users":"null","id":""},"workflow_variables":[{"description":"","id":"a034abee-5a5f-4347-9e58-6d2e58ce70f2","name":"This is a test","value":"{\"name\": \"testing\", \"name2\": \"testing3\", \"loop\": [{\"data\": \"1.1.1.1\", \"data_type\": \"ip\"}, {\"data\": \"2.2.2.2\", \"data_type\": \"ip\"}]}"}]},"results":[{"action":{"app_name":"Testing","app_version":"1.0.0","app_id":"5411f573-9bba-44c4-a8d3-0e2bb704546d","errors":"null","id":"40447f30-fa44-4a4f-a133-4ee710368737","is_valid":True,"isStartNode":True,"sharing":True,"private_id":"","label":"Hello this is a name","small_image":"","large_image":"","environment":"Shuffle","name":"repeat_back_to_me","parameters":[{"description":"The message to repeat","id":"","name":"call","example":"","value":"testing is not the same as testing2 \n\n\nNot list $this is a test.loop.# either","multiline":"false","action_field":"","variant":"STATIC_VALUE","required":True,"schema":{"type":"string"}}],"execution_variable":{"description":"","id":"","name":"","value":""},"position":{"x":360.5,"y":454.5},"priority":0},"execution_id":"083eaa87-17ff-4aba-996c-83245051cf3d","authorization":"b1264cd6-ed7a-4839-8caf-187eee8804b9","result":"TypeError: list indices must be integers or slices, not str","started_at":1593236790,"completed_at":1593236790,"status":"FAILURE"}]} # Takes a workflow execution as argument @@ -18,18 +16,52 @@ def get_json_value(execution_data, input_data): # 1. Find the action baseresult = "" + actionname_lower = actionname.lower() try: - if actionname.lower() == "exec": + if actionname_lower == "exec": baseresult = execution_data["execution_argument"] else: for result in execution_data["results"]: resultlabel = result["action"]["label"].replace(" ", "_", -1).lower() - if resultlabel.lower() == actionname.lower(): + if resultlabel.lower() == actionname_lower: baseresult = result["result"] break + + print("BEFORE VARIABLES!") + if len(baseresult) == 0: + try: + #print("WF Variables: %s" % execution_data["workflow"]["workflow_variables"]) + for variable in execution_data["workflow"]["workflow_variables"]: + variablename = variable["name"].replace(" ", "_", -1).lower() + + if variablename.lower() == actionname_lower: + baseresult = variable["value"] + break + except KeyError as e: + print("KeyError wf variables: %s" % e) + pass + except TypeError as e: + print("TypeError wf variables: %s" % e) + pass + + print("BEFORE EXECUTION VAR") + if len(baseresult) == 0: + try: + #print("Execution Variables: %s" % execution_data["execution_variables"]) + for variable in execution_data["execution_variables"]: + variablename = variable["name"].replace(" ", "_", -1).lower() + if variablename.lower() == actionname_lower: + baseresult = variable["value"] + break + except KeyError as e: + print("KeyError exec variables: %s" % e) + pass + except TypeError as e: + print("TypeError exec variables: %s" % e) + pass except KeyError as error: - print(f"Error: {error}") + print(f"KeyError in JSON: {error}") print(f"After first trycatch") @@ -47,23 +79,44 @@ def get_json_value(execution_data, input_data): except json.decoder.JSONDecodeError as e: return baseresult + # This whole thing should be recursive. try: cnt = 0 for value in parsersplit[1:]: cnt += 1 + print("VALUE: %s" % value) if value == "#": # FIXME - not recursive - should go deeper if there are more # - print("HANDLE RECURSIVE LOOP ") + print("HANDLE RECURSIVE LOOP OF %s" % basejson) returnlist = [] - for innervalue in basejson: - #print("Value: %s" % value[parsersplit[cnt+1]]) - returnlist.append(innervalue[parsersplit[cnt+1]]) + try: + for innervalue in basejson: + print("Value: %s" % innervalue[parsersplit[cnt+1]]) + returnlist.append(innervalue[parsersplit[cnt+1]]) + except IndexError as e: + print("Indexerror inner: %s" % e) + # Basically means its a normal list, not a crazy one :) + # Custom format for ${name[0,1,2,...]}$ + indexvalue = "${NO_SPLITTER%s}$" % json.dumps(basejson) + if len(returnlist) > 0: + indexvalue = "${NO_SPLITTER%s}$" % json.dumps(returnlist) + + print("INDEXVAL: ", indexvalue) + return indexvalue + except TypeError as e: + print("TypeError inner: %s" % e) # Example format: ${[]}$ - return "${%s%s}$" % (parsersplit[cnt+1], json.dumps(returnlist)) + parseditem = "${%s%s}$" % (parsersplit[cnt+1], json.dumps(returnlist)) + print("PARSED LOOP ITEM: %s" % parseditem) + return parseditem else: + print("BEFORE NORMAL VALUE: ", basejson, value) + if len(value) == 0: + return basejson + if isinstance(basejson[value], str): print(f"LOADING STRING '%s' AS JSON" % basejson[value]) try: @@ -75,25 +128,20 @@ def get_json_value(execution_data, input_data): basejson = basejson[value] except KeyError as e: - return "KeyError: %s" % e - except IndexError as e: - return "IndexError: %s" % e + print("Lower keyerror: %s" % e) + return "KeyError: Couldn't find key: %s" % e return basejson parameter = { "value": """{ - "data": "$exec.test", - "lul": "helo", - "data2": "asd", - "data3": "helo2j", - "data5": "$testing_1.#.title", - "data7": "$testing 1.#.id" + "data8": "Not list $this is a test.loop either with the items $this is a test.loop.#.data_type" }""" } -match = ".*([$]{1}([a-zA-Z0-9()# _-]+\.?){1,})" +match = ".*?([$]{1}([a-zA-Z0-9 _-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,})" actualitem = re.findall(match, parameter["value"], re.MULTILINE) +print("ACTUAL: ", actualitem) if len(actualitem) > 0: data = parameter["value"] From eed0d96ed078b84a81926d1231239eb82d7d0db6 Mon Sep 17 00:00:00 2001 From: frikky Date: Sun, 28 Jun 2020 19:42:08 +0200 Subject: [PATCH 14/14] Pushing for blogpost 4 update --- backend/app_sdk/app_base.py | 33 ++++++++++++++++++----------- backend/go-app/walkoff.go | 2 +- backend/tests/forparser.py | 4 ++-- docker-compose.yml | 2 +- frontend/src/AngularWorkflow.js | 13 ++++++------ functions/onprem/orborus/orborus.go | 5 ++++- 6 files changed, 36 insertions(+), 23 deletions(-) diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index c39233a8..89f1f0a7 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -438,10 +438,9 @@ class AppBase: print("Lower keyerror: %s" % e) return "KeyError: Couldn't find key: %s" % e - return basejson - - + return basejson + # Parses parameters sent to it and returns whether it did it successfully with the values found def parse_params(action, fullexecution, parameter): # Skip if it starts with $? jsonparsevalue = "$." @@ -748,7 +747,7 @@ class AppBase: #submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" actualitem = re.findall(submatch, value, re.MULTILINE) - print("Multicheck: %s", actualitem) + print("Multicheck ", actualitem) if len(actualitem) > 0: multiexecution = True @@ -765,9 +764,12 @@ class AppBase: except IndexError: continue - itemlist = json.loads(actualitem) - if len(itemlist) > minlength: - minlength = len(itemlist) + try: + itemlist = json.loads(actualitem) + if len(itemlist) > minlength: + minlength = len(itemlist) + except json.decoder.JSONDecodeError as e: + print("JSON Error: %s in %s" % (e, actualitem)) replacements[to_be_replaced] = actualitem @@ -777,7 +779,13 @@ class AppBase: for i in range(0, minlength): tmpitem = json.loads(json.dumps(parameter["value"])) for key, value in replacements.items(): - replacement = json.loads(value)[i] + replacement = json.dumps(json.loads(value)[i]) + if replacement.startswith("\"") and replacement.endswith("\""): + replacement = replacement[1:len(replacement)-1] + #except json.decoder.JSONDecodeError as e: + + print("REPLACING %s with %s" % (key, replacement)) + #replacement = parse_wrapper_start(replacement) tmpitem = tmpitem.replace(key, replacement, -1) resultarray.append(tmpitem) @@ -786,7 +794,7 @@ class AppBase: multi_parameters[parameter["name"]] = resultarray else: # Parses things like int(value) - self.logger.info("Parsing wrapper data") + self.logger.info("Parsing wrapper data for %s" % value) value = parse_wrapper_start(value) params[parameter["name"]] = value @@ -806,6 +814,7 @@ class AppBase: except ValueError: result += "Failed autocasting. Can't handle %s type from function. Must be string" % type(newres) print("Can't handle type %s value from function" % (type(newres))) + print("POST NEWRES: ", newres) else: print("APP_SDK DONE: Starting MULTI execution with", multi_parameters) # 1. Use number of executions based on longest array @@ -830,7 +839,8 @@ class AppBase: #print("Running with params %s" % baseparams) ret = await func(**baseparams) - print("Inner ret: %s" % ret) + ret = ret.replace("\"", "\\\"", -1) + print("Inner ret parsed: %s" % ret) try: results.append(json.loads(ret)) @@ -850,8 +860,7 @@ class AppBase: print("Normal result?") result = results - print("RESULT: %s" % result) - + print("RESULT: %s" % result) action_result["status"] = "SUCCESS" action_result["result"] = str(result) if action_result["result"] == "": diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 5aa2aa45..36b78bd4 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -1573,7 +1573,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { // doesn't check sharing=true // Have to do it like this to add the user's apps log.Println("Apps set starting") - log.Printf("EXIT ON ERROR: %#v", workflow.Configuration.ExitOnError) + //log.Printf("EXIT ON ERROR: %#v", workflow.Configuration.ExitOnError) workflowApps := []WorkflowApp{} //memcacheName = "all_apps" //if item, err := memcache.Get(ctx, memcacheName); err == memcache.ErrCacheMiss { diff --git a/backend/tests/forparser.py b/backend/tests/forparser.py index ec748b56..1b898c6d 100644 --- a/backend/tests/forparser.py +++ b/backend/tests/forparser.py @@ -1,7 +1,7 @@ import re import json -fullexecution = {"type":"workflow","status":"FINISHED","start":"40447f30-fa44-4a4f-a133-4ee710368737","execution_argument":"","execution_id":"083eaa87-17ff-4aba-996c-83245051cf3d","execution_source":"default","workflow_id":"d7b73e8a-08fe-460e-987b-971cb6f1857f","last_node":"40447f30-fa44-4a4f-a133-4ee710368737","authorization":"b1264cd6-ed7a-4839-8caf-187eee8804b9","result":"TypeError: list indices must be integers or slices, not str","started_at":1593236788,"completed_at":1593236790,"project_id":"shuffle","locations":["europe-west2"],"workflow":{"actions":[{"app_name":"Testing","app_version":"1.0.0","app_id":"5411f573-9bba-44c4-a8d3-0e2bb704546d","errors":"null","id":"40447f30-fa44-4a4f-a133-4ee710368737","is_valid":True,"isStartNode":True,"sharing":True,"private_id":"","label":"Hello this is a name","small_image":"","large_image":"","environment":"Shuffle","name":"repeat_back_to_me","parameters":[{"description":"The message to repeat","id":"","name":"call","example":"REPEATING: Hello world","value":"$this is a test.name is not the same as $this is a test.name2 \n\n\nNot list $this is a test.loop.# either","multiline":True,"action_field":"","variant":"STATIC_VALUE","required":True,"schema":{"type":"string"}}],"execution_variable":{"description":"","id":"","name":"","value":""},"position":{"x":360.5,"y":454.5},"priority":0}],"branches":"null","triggers":"null","schedules":"null","configuration":{"exit_on_error":False,"start_from_top":False},"id":"d7b73e8a-08fe-460e-987b-971cb6f1857f","is_valid":True,"name":"App sdk parser testing","description":"","start":"40447f30-fa44-4a4f-a133-4ee710368737","owner":"43c36230-0a6e-40fc-aebc-a8ef57c81a88","sharing":"private","execution_org":{"name":"","org":"","users":"null","id":""},"workflow_variables":[{"description":"","id":"a034abee-5a5f-4347-9e58-6d2e58ce70f2","name":"This is a test","value":"{\"name\": \"testing\", \"name2\": \"testing3\", \"loop\": [{\"data\": \"1.1.1.1\", \"data_type\": \"ip\"}, {\"data\": \"2.2.2.2\", \"data_type\": \"ip\"}]}"}]},"results":[{"action":{"app_name":"Testing","app_version":"1.0.0","app_id":"5411f573-9bba-44c4-a8d3-0e2bb704546d","errors":"null","id":"40447f30-fa44-4a4f-a133-4ee710368737","is_valid":True,"isStartNode":True,"sharing":True,"private_id":"","label":"Hello this is a name","small_image":"","large_image":"","environment":"Shuffle","name":"repeat_back_to_me","parameters":[{"description":"The message to repeat","id":"","name":"call","example":"","value":"testing is not the same as testing2 \n\n\nNot list $this is a test.loop.# either","multiline":"false","action_field":"","variant":"STATIC_VALUE","required":True,"schema":{"type":"string"}}],"execution_variable":{"description":"","id":"","name":"","value":""},"position":{"x":360.5,"y":454.5},"priority":0},"execution_id":"083eaa87-17ff-4aba-996c-83245051cf3d","authorization":"b1264cd6-ed7a-4839-8caf-187eee8804b9","result":"TypeError: list indices must be integers or slices, not str","started_at":1593236790,"completed_at":1593236790,"status":"FAILURE"}]} +fullexecution = {"type":"workflow","status":"FINISHED","start":"40447f30-fa44-4a4f-a133-4ee710368737","execution_argument":"","execution_id":"083eaa87-17ff-4aba-996c-83245051cf3d","execution_source":"default","workflow_id":"d7b73e8a-08fe-460e-987b-971cb6f1857f","last_node":"40447f30-fa44-4a4f-a133-4ee710368737","authorization":"b1264cd6-ed7a-4839-8caf-187eee8804b9","result":"TypeError: list indices must be integers or slices, not str","started_at":1593236788,"completed_at":1593236790,"project_id":"shuffle","locations":["europe-west2"],"workflow":{"actions":[{"app_name":"Testing","app_version":"1.0.0","app_id":"5411f573-9bba-44c4-a8d3-0e2bb704546d","errors":"null","id":"40447f30-fa44-4a4f-a133-4ee710368737","is_valid":True,"isStartNode":True,"sharing":True,"private_id":"","label":"Hello this is a name","small_image":"","large_image":"","environment":"Shuffle","name":"repeat_back_to_me","parameters":[{"description":"The message to repeat","id":"","name":"call","example":"REPEATING: Hello world","value":"$this is a test.name is not the same as $this is a test.name2 \n\n\nNot list $this is a test.loop.# either","multiline":True,"action_field":"","variant":"STATIC_VALUE","required":True,"schema":{"type":"string"}}],"execution_variable":{"description":"","id":"","name":"","value":""},"position":{"x":360.5,"y":454.5},"priority":0}],"branches":"null","triggers":"null","schedules":"null","configuration":{"exit_on_error":False,"start_from_top":False},"id":"d7b73e8a-08fe-460e-987b-971cb6f1857f","is_valid":True,"name":"App sdk parser testing","description":"","start":"40447f30-fa44-4a4f-a133-4ee710368737","owner":"43c36230-0a6e-40fc-aebc-a8ef57c81a88","sharing":"private","execution_org":{"name":"","org":"","users":"null","id":""},"workflow_variables":[{"description":"","id":"a034abee-5a5f-4347-9e58-6d2e58ce70f2","name":"This is a test","value":"[{\"data\": \"1.1.1.1\", \"data_type\": \"ip\"}]"}]},"results":[{"action":{"app_name":"Testing","app_version":"1.0.0","app_id":"5411f573-9bba-44c4-a8d3-0e2bb704546d","errors":"null","id":"40447f30-fa44-4a4f-a133-4ee710368737","is_valid":True,"isStartNode":True,"sharing":True,"private_id":"","label":"Hello this is a name","small_image":"","large_image":"","environment":"Shuffle","name":"repeat_back_to_me","parameters":[{"description":"The message to repeat","id":"","name":"call","example":"","value":"testing is not the same as testing2 \n\n\nNot list $this is a test.loop.# either","multiline":"false","action_field":"","variant":"STATIC_VALUE","required":True,"schema":{"type":"string"}}],"execution_variable":{"description":"","id":"","name":"","value":""},"position":{"x":360.5,"y":454.5},"priority":0},"execution_id":"083eaa87-17ff-4aba-996c-83245051cf3d","authorization":"b1264cd6-ed7a-4839-8caf-187eee8804b9","result":"TypeError: list indices must be integers or slices, not str","started_at":1593236790,"completed_at":1593236790,"status":"FAILURE"}]} # Takes a workflow execution as argument @@ -135,7 +135,7 @@ def get_json_value(execution_data, input_data): parameter = { "value": """{ - "data8": "Not list $this is a test.loop either with the items $this is a test.loop.#.data_type" + "data8": "Not list $this is a test.#.data either with the items $this is a test.#.data_type" }""" } diff --git a/docker-compose.yml b/docker-compose.yml index c13e60fc..b74d7a3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: depends_on: - backend backend: - build: ./backend + #build: ./backend image: frikky/shuffle:backend container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} diff --git a/frontend/src/AngularWorkflow.js b/frontend/src/AngularWorkflow.js index db735647..ff9d1d5c 100644 --- a/frontend/src/AngularWorkflow.js +++ b/frontend/src/AngularWorkflow.js @@ -35,6 +35,7 @@ import Switch from '@material-ui/core/Switch'; import ReactJson from 'react-json-view' import { useBeforeunload } from 'react-beforeunload'; +import CachedIcon from '@material-ui/icons/Cached'; import DirectionsRunIcon from '@material-ui/icons/DirectionsRun'; import PolymerIcon from '@material-ui/icons/Polymer'; import CreateIcon from '@material-ui/icons/Create'; @@ -4305,11 +4306,9 @@ const AngularWorkflow = (props) => { />
- + @@ -4562,6 +4561,7 @@ const AngularWorkflow = (props) => { onClick={() => { getWorkflowExecution(props.match.params.key) }} color="primary"> + Refresh executions @@ -4664,7 +4664,7 @@ const AngularWorkflow = (props) => { : executionData.results.map(data => { - if (!showSkippedActions && (data.status === "SKIPPED" || data.status === "FAILURE")) { + if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED" || data.status === "FAILURE")) { return null } @@ -4685,9 +4685,10 @@ const AngularWorkflow = (props) => { const curapp = apps.find(a => a.name === data.action.app_name && a.app_version === data.action.app_version) const imgsize = 50 + const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? "green" : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : "orange" const actionimg = curapp === null ? null : - {data.action.app_name} + {data.action.app_name} return (
diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 9cd19826..aed440b6 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -432,7 +432,6 @@ func zombiecheck() error { stopContainers := []string{} removeContainers := []string{} for _, container := range containers { - // Skip random containers. Only handle things related to Shuffle. if !strings.Contains(container.Image, baseimagename) { shuffleFound := false @@ -447,6 +446,8 @@ func zombiecheck() error { if !shuffleFound { continue } + //} else { + // log.Printf("NAME: %s", container.Image) } for _, name := range container.Names { @@ -455,6 +456,8 @@ func zombiecheck() error { continue } + log.Printf("NAME: %s", name) + // Need to check time here too because a container can be removed the same instant as its created currenttime := time.Now().Unix() if container.State != "running" && currenttime-container.Created > int64(workerTimeout) {