From 4305b8275d9b42cb651869948dd9588908736d16 Mon Sep 17 00:00:00 2001 From: frikky Date: Sat, 13 Mar 2021 18:39:33 +0100 Subject: [PATCH] Started re-adding oauth2 configurations --- backend/app_sdk/app_base.py | 98 ++++- backend/go-app/main.go | 558 +------------------------ backend/go-app/oauth2.go | 468 +++++++++++++++++++++ backend/go-app/walkoff.go | 16 +- frontend/src/views/AngularWorkflow.jsx | 114 ++--- 5 files changed, 633 insertions(+), 621 deletions(-) create mode 100644 backend/go-app/oauth2.go diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index b3cd2f7e..b9eee023 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -28,6 +28,7 @@ class AppBase: self.authorization = os.getenv("AUTHORIZATION", "") self.current_execution_id = os.getenv("EXECUTIONID", "") self.full_execution = os.getenv("FULL_EXECUTION", "") + self.start_time = int(time.time()) self.result_wrapper_count = 0 if isinstance(self.action, str): @@ -482,10 +483,17 @@ class AppBase: print(f"NEW PARAMS: {new_params}") if len(new_params) == 0: print("[WARNING] SHOULD STOP MULTI-EXECUTION BECAUSE FIELDS AREN'T UNIQUE") - action_result["status"] = "SKIPPED" - action_result["result"] = f"All values were non-unique" - action_result["completed_at"] = int(time.time()) - self.send_result(action_result, headers, stream_path) + action_result = { + "action": self.action, + "authorization": self.authorization, + "execution_id": self.current_execution_id, + "result": f"All {len(param_multiplier)} values were non-unique", + "started_at": self.start_time, + "status": "SKIPPED", + "completed_at": int(time.time()), + } + + self.send_result(action_result, {"Content-Type": "application/json", "Authorization": "Bearer %s" % self.authorization}, "/api/v1/streams") exit() #return else: @@ -906,6 +914,41 @@ class AppBase: return data.strip() if "split" in thistype: return data.split() + if "join" in thistype: + print(f"SHOULD JOIN: {data}") + try: + splitvalues = data.split(",") + if "," not in data: + return f"join({data})" + + if len(splitvalues) >= 2: + print(f"SPLITVALUE: {splitvalues[-1]}") + + # 1. Take the list and parse it from string + # 2. Take all the items and join them + # 3. Parse them back as string and return + values = ",".join(splitvalues[0:-1]) + print(f"VALUES: {values}") + tmp = json.loads(values) + print(f"TMP: {tmp}") + #tmp = tmp[1:-1] + #print(f"TMP2: {tmp}") + try: + newvalues = splitvalues[-1].join(str(item).strip() for item in tmp) + except TypeError: + newvalues = splitvalues[-1].join(json.dumps(item).strip() for item in tmp) + + print(f"new: {newvalues}") + return newvalues + else: + print("Returning default") + return f"join({data})" + + except (KeyError, IndexError) as e: + print(f"ERROR in join(): {e}") + except json.decoder.JSONDecodeError as e: + print(f"JSON ERROR in join(): {e}") + if "len" in thistype or "length" in thistype or "lenght" in thistype: tmp = "" try: @@ -919,9 +962,9 @@ class AppBase: pass if isinstance(tmp, list): - return len(tmp) + return str(len(tmp)) elif isinstance(tmp, object): - return len(tmp) + return str(len(tmp)) return str(len(data)) if "parse" in thistype: @@ -967,7 +1010,7 @@ class AppBase: #print("Running %s" % data) # Look for the INNER wrapper first, then move out - wrappers = ["int", "number", "lower", "upper", "trim", "strip", "split", "parse", "len", "length", "lenght"] + wrappers = ["int", "number", "lower", "upper", "trim", "strip", "split", "parse", "len", "length", "lenght", "join"] found = False for wrapper in wrappers: if wrapper not in data.lower(): @@ -982,8 +1025,8 @@ class AppBase: # Do stuff here. innervalue = parse_nested_param(data, maxDepth(data)-0) outervalue = parse_nested_param(data, maxDepth(data)-1) - #print("INNER: ", innervalue) - #print("OUTER: ", outervalue) + print("INNER: ", innervalue) + print("OUTER: ", outervalue) if outervalue != innervalue: #print("Outer: ", outervalue, " inner: ", innervalue) @@ -1224,10 +1267,10 @@ class AppBase: baseresult = variable["value"] break except KeyError as e: - print("KeyError wf variables: %s" % e) + print("[INFO] KeyError wf variables: %s" % e) pass except TypeError as e: - print("TypeError wf variables: %s" % e) + print("[INFO] TypeError wf variables: %s" % e) pass print("BEFORE EXECUTION VAR") @@ -1240,10 +1283,10 @@ class AppBase: baseresult = variable["value"] break except KeyError as e: - print("KeyError exec variables: %s" % e) + print("[INFO] KeyError exec variables: %s" % e) pass except TypeError as e: - print("TypeError exec variables: %s" % e) + print("[INFO] TypeError exec variables: %s" % e) pass except KeyError as error: @@ -1701,7 +1744,8 @@ class AppBase: # Custom format for ${name[0,1,2,...]}$ #submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" print(f"Returnedvalue: {value}") - #submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" + # OLD: Used until 13.03.2021: submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" + # \${[0-9a-zA-Z_-]+#?(\[.*?]}\$) submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*?]}\$))" actualitem = re.findall(submatch, value, re.MULTILINE) try: @@ -1722,18 +1766,24 @@ class AppBase: # Loop WITH variables go in else. print("Before first part in multiexec!") handled = False + + # Has a loop without a variable used inside if len(actualitem[0]) > 2 and actualitem[0][1] == "SHUFFLE_NO_SPLITTER": print("(1) Pre replacement: %s" % actualitem[0][2]) tmpitem = value - replacement = actualitem[0][2] + index = 0 + replacement = actualitem[index][2] + if replacement.endswith("}$"): + replacement = replacement[:-2] + if replacement.startswith("\"") and replacement.endswith("\""): replacement = replacement[1:len(replacement)-1] print("POST replacement: %s" % replacement) - #json_replacement = tmpitem.replace(actualitem[0][0], replacement, 1) + #json_replacement = tmpitem.replace(actualitem[index][0], replacement, 1) #print("AFTER POST replacement: %s" % json_replacement) #json_replacement = replacement try: @@ -1755,9 +1805,9 @@ class AppBase: for i in range(len(json_replacement)): if isinstance(json_replacement[i], dict) or isinstance(json_replacement[i], list): tmp_replacer = json.dumps(json_replacement[i]) - newvalue = tmpitem.replace(actualitem[0][0], tmp_replacer, 1) + newvalue = tmpitem.replace(actualitem[index][0], tmp_replacer, 1) else: - newvalue = tmpitem.replace(actualitem[0][0], json_replacement[i], 1) + newvalue = tmpitem.replace(actualitem[index][0], json_replacement[i], 1) try: newvalue = json.loads(newvalue) @@ -1770,7 +1820,7 @@ class AppBase: print("New replacement: %s" % new_replacement) # New - tmpitem = tmpitem.replace(actualitem[0][0], replacement, 1) + tmpitem = tmpitem.replace(actualitem[index][0], replacement, 1) # This code handles files. resultarray = [] @@ -1821,9 +1871,15 @@ class AppBase: try: to_be_replaced = replace[0] actualitem = replace[2] + if actualitem.endswith("}$"): + actualitem = actualitem[:-2] except IndexError: continue + #print(f"\n\nTMPITEM: {actualitem}\n\n") + #actualitem = parse_wrapper_start(actualitem) + #print(f"\n\nTMPITEM2: {actualitem}\n\n") + try: itemlist = json.loads(actualitem) if len(itemlist) > minlength: @@ -1837,6 +1893,10 @@ class AppBase: replacements[to_be_replaced] = actualitem + + # Parses the data as string with length, split etc. before moving on. + + #print("In second part of else: %s" % (len(itemlist))) # This is a result array for JUST this value.. # What if there are more? diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 57d57e75..7dc85deb 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -42,7 +42,6 @@ import ( */ "github.com/google/go-github/v28/github" - "golang.org/x/oauth2" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" @@ -671,7 +670,7 @@ func handleApiAuthentication(resp http.ResponseWriter, request *http.Request) (U // Should basically never happen Userdata, err := getUser(ctx, session.Id) if err != nil { - log.Printf("Username %s doesn't exist (authcheck): %s", session.Username, err) + log.Printf("[INFO] Username %s doesn't exist (authcheck): %s", session.Username, err) return User{}, err } @@ -1770,90 +1769,6 @@ type passwordChange struct { Currentpassword string `json:"currentpassword"` } -func handlePasswordResetMail(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - log.Println("Handling password reset mail") - defaultMessage := "We have sent you an email :)" - - 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": "%s"}`, defaultMessage))) - return - } - - type passwordReset struct { - Username string `json:"username"` - } - - var t passwordReset - err = json.Unmarshal(body, &t) - if err != nil { - log.Printf("Failed unmarshaling: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, defaultMessage))) - return - } - - ctx := context.Background() - Userdata, err := getUser(ctx, t.Username) - if err != nil { - log.Printf("Username %s doesn't exist (pw reset mail): %s", t.Username, err) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) - return - } - - resetToken := uuid.NewV4() - // FIXME: - // Weakness with this system is that you can spam someone with password resets, - // and they would never be able to reset, as a new token is always generated - url := fmt.Sprintf("https://shuffler.io/passwordreset/%s", resetToken.String()) - - Userdata.ResetReference = resetToken.String() - Userdata.ResetTimeout = 0 - err = setUser(ctx, Userdata) - if err != nil { - log.Printf("Error patching User for mail %s: %s", Userdata.Username, err) - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) - return - } - - log.Printf("%#v", Userdata) - addr := t.Username - const confirmMessage = ` -Reset URL :) - -%s - ` - - msg := &mail.Message{ - Sender: "Shuffle ", - To: []string{addr}, - Subject: "Reset your password - Shuffle", - Body: fmt.Sprintf(confirmMessage, url), - } - - log.Println(msg.Body) - if err := mail.Send(ctx, msg); err != nil { - log.Printf("Couldn't send email: %v", err) - } - - // FIXME - // Generate an email to send - // Generate a reset code with a reset link - // Build frontend to handle reset link with "new password" etc. - - resp.WriteHeader(200) - resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "%s"}`, defaultMessage))) -} - func handlePasswordReset(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -4954,398 +4869,6 @@ func getDocs(resp http.ResponseWriter, request *http.Request) { resp.Write(b) } -type OutlookProfile struct { - OdataContext string `json:"@odata.context"` - BusinessPhones []string `json:"businessPhones"` - DisplayName string `json:"displayName"` - GivenName string `json:"givenName"` - JobTitle interface{} `json:"jobTitle"` - Mail string `json:"mail"` - MobilePhone interface{} `json:"mobilePhone"` - OfficeLocation interface{} `json:"officeLocation"` - PreferredLanguage interface{} `json:"preferredLanguage"` - Surname string `json:"surname"` - UserPrincipalName string `json:"userPrincipalName"` - ID string `json:"id"` -} - -type OutlookFolder struct { - ID string `json:"id"` - DisplayName string `json:"displayName"` - ParentFolderID string `json:"parentFolderId"` - ChildFolderCount int `json:"childFolderCount"` - UnreadItemCount int `json:"unreadItemCount"` - TotalItemCount int `json:"totalItemCount"` -} - -type OutlookFolders struct { - OdataContext string `json:"@odata.context"` - OdataNextLink string `json:"@odata.nextLink"` - Value []OutlookFolder `json:"value"` -} - -func getOutlookFolders(client *http.Client) (OutlookFolders, error) { - requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/frikky@shuffletest.onmicrosoft.com/mailfolders") - - ret, err := client.Get(requestUrl) - if err != nil { - log.Printf("FolderErr: %s", err) - return OutlookFolders{}, err - } - - if ret.StatusCode != 200 { - log.Printf("Status folders: %d", ret.StatusCode) - return OutlookFolders{}, err - } - - body, err := ioutil.ReadAll(ret.Body) - if err != nil { - log.Printf("Body: %s", err) - return OutlookFolders{}, err - } - - //log.Printf("Body: %s", string(body)) - - mailfolders := OutlookFolders{} - err = json.Unmarshal(body, &mailfolders) - if err != nil { - log.Printf("Unmarshal: %s", err) - return OutlookFolders{}, err - } - - //fmt.Printf("%#v", mailfolders) - // FIXME - recursion for subfolders - // Recursive struct - // folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId) - //for _, folder := range mailfolders.Value { - // log.Println(folder.DisplayName) - //} - - return mailfolders, nil -} - -func getOutlookProfile(client *http.Client) (OutlookProfile, error) { - requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me?$select=mail") - - ret, err := client.Get(requestUrl) - if err != nil { - log.Printf("FolderErr: %s", err) - return OutlookProfile{}, err - } - - log.Printf("Status folders: %d", ret.StatusCode) - body, err := ioutil.ReadAll(ret.Body) - if err != nil { - log.Printf("Body: %s", err) - return OutlookProfile{}, err - } - - profile := OutlookProfile{} - err = json.Unmarshal(body, &profile) - if err != nil { - log.Printf("Unmarshal: %s", err) - return OutlookProfile{}, err - } - - return profile, nil -} - -func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { - code := request.URL.Query().Get("code") - if len(code) == 0 { - log.Println("No code") - resp.WriteHeader(401) - return - } - - url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) - log.Println(url) - ctx := context.Background() - client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) - if err != nil { - log.Printf("Oauth client failure - outlook register: %s", err) - resp.WriteHeader(401) - return - } - // This should be possible, and will also give the actual username - profile, err := getOutlookProfile(client) - if err != nil { - log.Printf("Outlook profile failure: %s", err) - resp.WriteHeader(401) - return - } - - // This is a state workaround, which should really be for CSRF checks lol - state := request.URL.Query().Get("state") - if len(state) == 0 { - log.Println("No state") - resp.WriteHeader(401) - return - } - - stateitems := strings.Split(state, "%26") - if len(stateitems) == 1 { - stateitems = strings.Split(state, "&") - } - - // FIXME - trigger auth - senderUser := "" - trigger := TriggerAuth{} - for _, item := range stateitems { - itemsplit := strings.Split(item, "%3D") - if len(itemsplit) == 1 { - itemsplit = strings.Split(item, "=") - } - - if len(itemsplit) != 2 { - continue - } - - // Do something here - if itemsplit[0] == "workflow_id" { - trigger.WorkflowId = itemsplit[1] - } else if itemsplit[0] == "trigger_id" { - trigger.Id = itemsplit[1] - } else if itemsplit[0] == "type" { - trigger.Type = itemsplit[1] - } else if itemsplit[0] == "username" { - trigger.Username = itemsplit[1] - trigger.Owner = itemsplit[1] - senderUser = itemsplit[1] - } - } - - // THis is an override based on the user in oauth return - trigger.Username = profile.Mail - trigger.Code = code - trigger.OauthToken = OauthToken{ - AccessToken: accessToken.AccessToken, - TokenType: accessToken.TokenType, - RefreshToken: accessToken.RefreshToken, - Expiry: accessToken.Expiry, - } - - //log.Printf("%#v", trigger) - if trigger.WorkflowId == "" || trigger.Id == "" || trigger.Username == "" || trigger.Type == "" { - log.Printf("All oauth items need to contain data to register a new state") - resp.WriteHeader(401) - return - } - - // Should also update the user - Userdata, err := getUser(ctx, senderUser) - if err != nil { - log.Printf("Username %s doesn't exist (oauth2): %s", trigger.Username, err) - resp.WriteHeader(401) - return - } - - Userdata.Authentication = append(Userdata.Authentication, UserAuth{ - Name: "Outlook", - Description: "oauth2", - Workflows: []string{trigger.WorkflowId}, - Username: trigger.Username, - Fields: []UserAuthField{ - UserAuthField{ - Key: "trigger_id", - Value: trigger.Id, - }, - UserAuthField{ - Key: "username", - Value: trigger.Username, - }, - UserAuthField{ - Key: "code", - Value: code, - }, - UserAuthField{ - Key: "type", - Value: trigger.Type, - }, - }, - }) - - // Set apikey for the user if they don't have one - if len(Userdata.ApiKey) == 0 { - newUser, err := generateApikey(ctx, *Userdata) - Userdata = &newUser - if err != nil { - log.Printf("Failed to generate apikey for user %s when creating outlook sub: %s", Userdata.Username, err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - } - - //err = setUser(Userdata) - //if err != nil { - // log.Printf("Failed setting user data for %s: %s", Userdata.Username, err) - // resp.WriteHeader(401) - // return - //} - - err = setTriggerAuth(ctx, trigger) - if err != nil { - log.Printf("Failed to set trigger auth for %s - %s", trigger.Username, err) - resp.WriteHeader(401) - return - } - - // FIXME - not sure if these are good at all :) - environmentVariables := map[string]string{ - "FUNCTION_APIKEY": Userdata.ApiKey, - "CALLBACKURL": "https://shuffler.io", - "WORKFLOW_ID": trigger.WorkflowId, - "TRIGGER_ID": trigger.Id, - } - - applocation := fmt.Sprintf("gs://%s/triggers/outlooktrigger.zip", bucketName) - hookname := fmt.Sprintf("outlooktrigger_%s", trigger.Id) - - err = deployCloudFunctionGo(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 - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -type OauthToken struct { - AccessToken string `json:"AccessToken" datastore:"AccessToken,noindex"` - TokenType string `json:"TokenType" datastore:"TokenType,noindex"` - RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"` - Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"` -} -type TriggerAuth struct { - Id string `json:"id" datastore:"id"` - SubscriptionId string `json:"subscriptionId" datastore:"subscriptionId"` - - Username string `json:"username" datastore:"username,noindex"` - WorkflowId string `json:"workflow_id" datastore:"workflow_id,noindex"` - Owner string `json:"owner" datastore:"owner"` - Type string `json:"type" datastore:"type"` - Code string `json:"code,omitempty" datastore:"code,noindex"` - OauthToken OauthToken `json:"oauth_token,omitempty" datastore:"oauth_token"` -} - -func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) { - key := datastore.NameKey("trigger_auth", strings.ToLower(id), nil) - triggerauth := &TriggerAuth{} - if err := dbclient.Get(ctx, key, triggerauth); err != nil { - return &TriggerAuth{}, err - } - - return triggerauth, nil -} - -func setTriggerAuth(ctx context.Context, trigger TriggerAuth) error { - key1 := datastore.NameKey("trigger_auth", strings.ToLower(trigger.Id), nil) - - // New struct, to not add body, author etc - if _, err := dbclient.Put(ctx, key1, &trigger); err != nil { - log.Printf("Error adding trigger auth: %s", err) - return err - } - - return nil -} - -// THis all of a sudden became really horrible.. fml -func getOutlookClient(ctx context.Context, code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) { - - conf := &oauth2.Config{ - ClientID: "", - ClientSecret: "", - Scopes: []string{ - "Mail.Read", - "User.Read", - }, - RedirectURL: redirectUri, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize", - TokenURL: "https://login.microsoftonline.com/common/oauth2/token", - }, - } - - if len(code) > 0 { - access_token, err := conf.Exchange(ctx, code) - if err != nil { - log.Printf("Access_token issue: %s", err) - return &http.Client{}, access_token, err - } - - client := conf.Client(ctx, access_token) - return client, access_token, nil - } else { - // Manually recreate the oauthtoken - access_token := &oauth2.Token{ - AccessToken: accessToken.AccessToken, - TokenType: accessToken.TokenType, - RefreshToken: accessToken.RefreshToken, - Expiry: accessToken.Expiry, - } - - client := conf.Client(ctx, access_token) - return client, access_token, nil - } -} - -func handleGetOutlookFolders(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - // Exchange every time hmm - // FIXME - // Should really just get the code from the trigger that's being used OR the user - triggerId := request.URL.Query().Get("trigger_id") - if len(triggerId) == 0 { - log.Println("No trigger_id supplied") - resp.WriteHeader(401) - return - } - - ctx := context.Background() - trigger, err := getTriggerAuth(ctx, triggerId) - if err != nil { - log.Printf("Trigger %s doesn't exist - outlook folders.", triggerId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Trigger doesn't exist."}`)) - return - } - - // FIXME - should be shuffler in literally every case except testing lol - redirectDomain := "shuffler.io" - url := fmt.Sprintf("https://%s/functions/outlook/register", redirectDomain) - outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("Oauth client failure - outlook folders: %s", err) - resp.WriteHeader(401) - return - } - - folders, err := getOutlookFolders(outlookClient) - if err != nil { - resp.WriteHeader(401) - return - } - - b, err := json.Marshal(folders.Value) - if err != nil { - log.Println("Failed to marshal folderdata") - resp.WriteHeader(401) - return - } - - resp.WriteHeader(200) - resp.Write(b) -} - func handleGetSpecificStats(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -5395,66 +4918,6 @@ func handleGetSpecificStats(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(b)) } -func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in getting specific workflow: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - if location[1] == "api" { - if len(location) <= 4 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - } - - if strings.Contains(workflowId, "?") { - workflowId = strings.Split(workflowId, "?")[0] - } - - ctx := context.Background() - trigger, err := getTriggerAuth(ctx, workflowId) - if err != nil { - log.Printf("Trigger %s doesn't exist - specific trigger.", workflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": ""}`)) - return - } - - if user.Username != trigger.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for trigger %s", user.Username, trigger.Id) - resp.WriteHeader(401) - return - } - - trigger.OauthToken = OauthToken{} - trigger.Code = "" - - b, err := json.Marshal(trigger) - if err != nil { - log.Println("Failed to marshal data") - resp.WriteHeader(401) - return - } - - resp.WriteHeader(200) - resp.Write(b) -} - func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -7545,7 +7008,7 @@ func runInit(ctx context.Context) { if err != nil { log.Printf("Failed loading repo %s into memory: %s", apis, err) } else { - log.Printf("Finished git clone. Looking for updates to the repo.") + log.Printf("[INFO] Finished git clone. Looking for updates to the repo.") dir, err := fs.ReadDir("") if err != nil { log.Printf("Failed reading folder: %s", err) @@ -7856,7 +7319,7 @@ func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { continue } - log.Printf("[INFO] Looking for value %s", value) + log.Printf("[INFO] Looking for value %s in Workflow %s of ORG %s", value, workflowExecution.Workflow.ID, org.Id) q := datastore.NewQuery(dbKey).Filter("org_id =", org.Id).Filter("workflow_id =", workflowExecution.Workflow.ID).Filter("parameter_name =", parameterNames).Filter("value =", value) foundCount, err := dbclient.Count(ctx, q) @@ -7875,7 +7338,7 @@ func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { } } } else { - log.Printf("[INFO] Should validate if value %s in app %s exists WITH ORG %s", workflowExecution.Workflow.ID) + //log.Printf("[INFO] Should validate if value %s in app %s exists WITH ORG %s", workflowExecution.Workflow.ID) for _, value := range value.ParameterValues { if len(value) == 0 { @@ -7883,7 +7346,7 @@ func handleKeyValueCheck(resp http.ResponseWriter, request *http.Request) { continue } - log.Printf("[INFO] Looking for value %s", value) + log.Printf("[INFO] Looking for value %s in ORG %s", value, org.Id) q := datastore.NewQuery(dbKey).Filter("org_id =", org.Id).Filter("workflow_id =", "").Filter("parameter_name =", parameterNames).Filter("value =", value) foundCount, err := dbclient.Count(ctx, q) @@ -8653,11 +8116,6 @@ func initHandlers() { r.HandleFunc("/api/v1/hooks/{key}", handleWebhookCallback).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/hooks/{key}/delete", handleDeleteHook).Methods("DELETE", "OPTIONS") - // Trigger hmm - //r.HandleFunc("/api/v1/triggers/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") - - r.HandleFunc("/api/v1/stats/{key}", handleGetSpecificStats).Methods("GET", "OPTIONS") - // OpenAPI configuration r.HandleFunc("/api/v1/verify_swagger", verifySwagger).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/verify_openapi", verifySwagger).Methods("POST", "OPTIONS") @@ -8693,6 +8151,12 @@ func initHandlers() { r.HandleFunc("/api/v1/files/{fileId}", handleDeleteFile).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/files", handleGetFiles).Methods("GET", "OPTIONS") + // Trigger hmm + r.HandleFunc("/api/v1/triggers/outlook/register", handleNewOutlookRegister).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/triggers/outlook/getFolders", handleGetOutlookFolders).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/triggers/outlook/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/stats/{key}", handleGetSpecificStats).Methods("GET", "OPTIONS") + http.Handle("/", r) } diff --git a/backend/go-app/oauth2.go b/backend/go-app/oauth2.go new file mode 100644 index 00000000..5f234e68 --- /dev/null +++ b/backend/go-app/oauth2.go @@ -0,0 +1,468 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "time" + + "cloud.google.com/go/datastore" + "golang.org/x/oauth2" +) + +type OutlookProfile struct { + OdataContext string `json:"@odata.context"` + BusinessPhones []string `json:"businessPhones"` + DisplayName string `json:"displayName"` + GivenName string `json:"givenName"` + JobTitle interface{} `json:"jobTitle"` + Mail string `json:"mail"` + MobilePhone interface{} `json:"mobilePhone"` + OfficeLocation interface{} `json:"officeLocation"` + PreferredLanguage interface{} `json:"preferredLanguage"` + Surname string `json:"surname"` + UserPrincipalName string `json:"userPrincipalName"` + ID string `json:"id"` +} + +type OutlookFolder struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + ParentFolderID string `json:"parentFolderId"` + ChildFolderCount int `json:"childFolderCount"` + UnreadItemCount int `json:"unreadItemCount"` + TotalItemCount int `json:"totalItemCount"` +} + +type OutlookFolders struct { + OdataContext string `json:"@odata.context"` + OdataNextLink string `json:"@odata.nextLink"` + Value []OutlookFolder `json:"value"` +} + +func getOutlookFolders(client *http.Client) (OutlookFolders, error) { + requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") + + ret, err := client.Get(requestUrl) + if err != nil { + log.Printf("[INFO] FolderErr: %s", err) + return OutlookFolders{}, err + } + + body, err := ioutil.ReadAll(ret.Body) + if err != nil { + log.Printf("[WARNING] Failed body decoding from mailfolders") + return OutlookFolders{}, err + } + + log.Printf("[INFO] Folder Body: %s", string(body)) + log.Printf("[INFO] Status folders: %d", ret.StatusCode) + if ret.StatusCode != 200 { + return OutlookFolders{}, err + } + + //log.Printf("Body: %s", string(body)) + + mailfolders := OutlookFolders{} + err = json.Unmarshal(body, &mailfolders) + if err != nil { + log.Printf("Unmarshal: %s", err) + return OutlookFolders{}, err + } + + //fmt.Printf("%#v", mailfolders) + // FIXME - recursion for subfolders + // Recursive struct + // folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId) + //for _, folder := range mailfolders.Value { + // log.Println(folder.DisplayName) + //} + + return mailfolders, nil +} + +func getOutlookProfile(client *http.Client) (OutlookProfile, error) { + requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me?$select=mail") + + ret, err := client.Get(requestUrl) + if err != nil { + log.Printf("[INFO] Folder error: %s", err) + return OutlookProfile{}, err + } + + log.Printf("[INFO] Status profile: %d", ret.StatusCode) + body, err := ioutil.ReadAll(ret.Body) + if err != nil { + log.Printf("[INFO] Body: %s", err) + return OutlookProfile{}, err + } + + log.Printf("[INFO] BODY: %s", string(body)) + + profile := OutlookProfile{} + err = json.Unmarshal(body, &profile) + if err != nil { + log.Printf("Unmarshal: %s", err) + return OutlookProfile{}, err + } + + return profile, nil +} + +func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { + code := request.URL.Query().Get("code") + if len(code) == 0 { + log.Println("No code") + resp.WriteHeader(401) + return + } + + url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) + log.Println(url) + ctx := context.Background() + client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) + if err != nil { + log.Printf("Oauth client failure - outlook register: %s", err) + resp.WriteHeader(401) + return + } + + // This should be possible, and will also give the actual username + profile, err := getOutlookProfile(client) + if err != nil { + log.Printf("Outlook profile failure: %s", err) + resp.WriteHeader(401) + return + } + + // This is a state workaround, which should really be for CSRF checks lol + state := request.URL.Query().Get("state") + if len(state) == 0 { + log.Println("No state") + resp.WriteHeader(401) + return + } + + stateitems := strings.Split(state, "%26") + if len(stateitems) == 1 { + stateitems = strings.Split(state, "&") + } + + // FIXME - trigger auth + senderUser := "" + trigger := TriggerAuth{} + for _, item := range stateitems { + itemsplit := strings.Split(item, "%3D") + if len(itemsplit) == 1 { + itemsplit = strings.Split(item, "=") + } + + if len(itemsplit) != 2 { + continue + } + + // Do something here + if itemsplit[0] == "workflow_id" { + trigger.WorkflowId = itemsplit[1] + } else if itemsplit[0] == "trigger_id" { + trigger.Id = itemsplit[1] + } else if itemsplit[0] == "type" { + trigger.Type = itemsplit[1] + } else if itemsplit[0] == "username" { + trigger.Username = itemsplit[1] + trigger.Owner = itemsplit[1] + senderUser = itemsplit[1] + } + } + + // THis is an override based on the user in oauth return + trigger.Username = profile.Mail + trigger.Code = code + trigger.OauthToken = OauthToken{ + AccessToken: accessToken.AccessToken, + TokenType: accessToken.TokenType, + RefreshToken: accessToken.RefreshToken, + Expiry: accessToken.Expiry, + } + + //log.Printf("%#v", trigger) + log.Println(trigger.WorkflowId) + log.Println(trigger.Id) + log.Println(trigger.Username) + log.Println(trigger.Type) + if trigger.WorkflowId == "" || trigger.Id == "" || trigger.Username == "" || trigger.Type == "" { + log.Printf("[INFO] All oauth items need to contain data to register a new state") + resp.WriteHeader(401) + return + } + + // Should also update the user + log.Printf("[INFO] Attempting to set up outlook trigger for %s", senderUser) + Userdata, err := getUser(ctx, senderUser) + if err != nil { + log.Printf("[INFO] Username %s doesn't exist (oauth2): %s", trigger.Username, err) + resp.WriteHeader(401) + return + } + + Userdata.Authentication = append(Userdata.Authentication, UserAuth{ + Name: "Outlook", + Description: "oauth2", + Workflows: []string{trigger.WorkflowId}, + Username: trigger.Username, + Fields: []UserAuthField{ + UserAuthField{ + Key: "trigger_id", + Value: trigger.Id, + }, + UserAuthField{ + Key: "username", + Value: trigger.Username, + }, + UserAuthField{ + Key: "code", + Value: code, + }, + UserAuthField{ + Key: "type", + Value: trigger.Type, + }, + }, + }) + + // Set apikey for the user if they don't have one + err = setUser(ctx, Userdata) + if err != nil { + log.Printf("Failed setting user data for %s: %s", Userdata.Username, err) + resp.WriteHeader(401) + return + } + + err = setTriggerAuth(ctx, trigger) + if err != nil { + log.Printf("Failed to set trigger auth for %s - %s", trigger.Username, err) + resp.WriteHeader(401) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(`{"success": true}`)) +} + +type OauthToken struct { + AccessToken string `json:"AccessToken" datastore:"AccessToken,noindex"` + TokenType string `json:"TokenType" datastore:"TokenType,noindex"` + RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"` + Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"` +} +type TriggerAuth struct { + Id string `json:"id" datastore:"id"` + SubscriptionId string `json:"subscriptionId" datastore:"subscriptionId"` + + Username string `json:"username" datastore:"username,noindex"` + WorkflowId string `json:"workflow_id" datastore:"workflow_id,noindex"` + Owner string `json:"owner" datastore:"owner"` + Type string `json:"type" datastore:"type"` + Code string `json:"code,omitempty" datastore:"code,noindex"` + OauthToken OauthToken `json:"oauth_token,omitempty" datastore:"oauth_token"` +} + +func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) { + key := datastore.NameKey("trigger_auth", strings.ToLower(id), nil) + triggerauth := &TriggerAuth{} + if err := dbclient.Get(ctx, key, triggerauth); err != nil { + return &TriggerAuth{}, err + } + + return triggerauth, nil +} + +func setTriggerAuth(ctx context.Context, trigger TriggerAuth) error { + key1 := datastore.NameKey("trigger_auth", strings.ToLower(trigger.Id), nil) + + // New struct, to not add body, author etc + if _, err := dbclient.Put(ctx, key1, &trigger); err != nil { + log.Printf("Error adding trigger auth: %s", err) + return err + } + + return nil +} + +// THis all of a sudden became really horrible.. fml +func getOutlookClient(ctx context.Context, code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) { + + conf := &oauth2.Config{ + ClientID: "fd55c175-aa30-4fa6-b303-09a29fb3f750", + ClientSecret: "14OBKgUpov.D7fe0~hp0z-cIQdP~SlYm.8", + Scopes: []string{ + "Mail.Read", + }, + RedirectURL: redirectUri, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize", + TokenURL: "https://login.microsoftonline.com/common/oauth2/token", + }, + } + + if len(code) > 0 { + access_token, err := conf.Exchange(ctx, code) + if err != nil { + log.Printf("Access_token issue: %s", err) + return &http.Client{}, access_token, err + } + + client := conf.Client(ctx, access_token) + return client, access_token, nil + } + + // Manually recreate the oauthtoken + access_token := &oauth2.Token{ + AccessToken: accessToken.AccessToken, + TokenType: accessToken.TokenType, + RefreshToken: accessToken.RefreshToken, + Expiry: accessToken.Expiry, + } + + client := conf.Client(ctx, access_token) + return client, access_token, nil +} + +func handleGetOutlookFolders(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + // Exchange every time hmm + // FIXME + // Should really just get the code from the trigger that's being used OR the user + triggerId := request.URL.Query().Get("trigger_id") + if len(triggerId) == 0 { + log.Println("No trigger_id supplied") + resp.WriteHeader(401) + return + } + + ctx := context.Background() + trigger, err := getTriggerAuth(ctx, triggerId) + if err != nil { + log.Printf("[INFO] Trigger %s doesn't exist - outlook folders.", triggerId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Trigger doesn't exist."}`)) + return + } + + //client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) + //if err != nil { + // log.Printf("Oauth client failure - outlook register: %s", err) + // resp.WriteHeader(401) + // return + //} + + // FIXME - should be shuffler in literally every case except testing lol + //log.Printf("TRIGGER: %#v", trigger) + redirectDomain := "localhost:5001" + url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) + outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) + if err != nil { + log.Printf("[WARNING] Oauth client failure - outlook folders: %s", err) + resp.Write([]byte(`{"success": false, "reason": "Failed creating outlook client"}`)) + resp.WriteHeader(401) + return + } + + // This should be possible, and will also give the actual username + /* + profile, err := getOutlookProfile(outlookClient) + if err != nil { + log.Printf("Outlook profile failure: %s", err) + resp.WriteHeader(401) + return + } + log.Printf("PROFILE: %#v", profile) + */ + + folders, err := getOutlookFolders(outlookClient) + if err != nil { + log.Printf("[WARNING] Failed setting outlook folders: %s", err) + resp.Write([]byte(`{"success": false, "reason": "Failed getting outlook folders"}`)) + resp.WriteHeader(401) + return + } + + b, err := json.Marshal(folders.Value) + if err != nil { + log.Println("[INFO] Failed to marshal folderdata") + resp.Write([]byte(`{"success": false, "reason": "Failed decoding JSON"}`)) + resp.WriteHeader(401) + return + } + + resp.WriteHeader(200) + resp.Write(b) +} + +func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in getting specific workflow: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + location := strings.Split(request.URL.String(), "/") + + var workflowId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + workflowId = location[5] + } + + if strings.Contains(workflowId, "?") { + workflowId = strings.Split(workflowId, "?")[0] + } + + ctx := context.Background() + trigger, err := getTriggerAuth(ctx, workflowId) + if err != nil { + log.Printf("[INFO] Trigger %s doesn't exist - specific trigger.", workflowId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": ""}`)) + return + } + + if user.Username != trigger.Owner && user.Role != "admin" { + log.Printf("Wrong user (%s) for trigger %s", user.Username, trigger.Id) + resp.WriteHeader(401) + return + } + + trigger.OauthToken = OauthToken{} + trigger.Code = "" + + b, err := json.Marshal(trigger) + if err != nil { + log.Println("Failed to marshal data") + resp.WriteHeader(401) + return + } + + resp.WriteHeader(200) + resp.Write(b) +} diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 3d9923b5..ac3265de 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -982,7 +982,7 @@ func validateNewWorkerExecution(body []byte) error { } if execution.Status == "EXECUTING" { - log.Printf("[INFO] Inside executing.") + //log.Printf("[INFO] Inside executing.") extra := 0 for _, trigger := range execution.Workflow.Triggers { //log.Printf("Appname trigger (0): %s", trigger.AppName) @@ -3992,7 +3992,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflow, err := getWorkflow(ctx, fileId) if err != nil { - log.Printf("Failed getting the workflow locally (stop schedule): %s", err) + log.Printf("[WARNING] Failed getting the workflow locally (stop schedule): %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4001,7 +4001,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { // FIXME - have a check for org etc too.. // FIXME - admin check like this? idk if user.Id != workflow.Owner && user.Role != "admin" && user.Role != "scheduler" { - log.Printf("Wrong user (%s) for workflow %s (stop schedule)", user.Username, workflow.ID) + log.Printf("[WARNING] Wrong user (%s) for workflow %s (stop schedule)", user.Username, workflow.ID) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4009,7 +4009,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { schedule, err := getSchedule(ctx, scheduleId) if err != nil { - log.Printf("Failed finding schedule %s", scheduleId) + log.Printf("[WARNING] Failed finding schedule %s", scheduleId) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -4039,15 +4039,15 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { err = executeCloudAction(action, org.SyncConfig.Apikey) if err != nil { - log.Printf("Failed cloud action STOP schedule: %s", err) + log.Printf("[WARNING] Failed cloud action STOP schedule: %s", err) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) return } else { - log.Printf("Successfully ran cloud action STOP schedule") + log.Printf("[INFO] Successfully ran cloud action STOP schedule") err = DeleteKey(ctx, "schedules", scheduleId) if err != nil { - log.Printf("Failed deleting cloud schedule onprem..") + log.Printf("[WARNING] Failed deleting cloud schedule onprem..") resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed deleting cloud schedule"}`))) return @@ -4061,7 +4061,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { err = deleteSchedule(ctx, scheduleId) if err != nil { - log.Printf("Failed deleting schedule: %s", err) + log.Printf("[WARNING] Failed deleting schedule: %s", err) if strings.Contains(err.Error(), "Job not found") { resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 45f44ba9..bde4fff9 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2307,10 +2307,12 @@ const AngularWorkflow = (props) => { return } + /* if (data.is_valid === false) { alert.error(data.name+" is only available on https://shuffler.io so far") return } + */ const triggerLabel = getNextActionName(data.name) @@ -4929,7 +4931,7 @@ const AngularWorkflow = (props) => { } const setFolders = () => { - fetch(globalUrl+"/functions/outlook/getFolders?trigger_id="+selectedTrigger.id, { + fetch(globalUrl+"/api/v1/triggers/outlook/getFolders?trigger_id="+selectedTrigger.id, { method: "GET", headers: {"content-type": "application/json"}, credentials: "include", @@ -4942,7 +4944,10 @@ const AngularWorkflow = (props) => { return response.json() }) .then((responseJson) => { - setTriggerFolders(responseJson) + if (responseJson !== null && responseJson.success !== false) { + setTriggerFolders(responseJson) + } + if (workflow.triggers[selectedTriggerIndex].parameters.length === 0 && responseJson.length > 0) { workflow.triggers[selectedTriggerIndex].parameters = [{"value": responseJson[0].displayName, "name": "outlookfolder", "id": responseJson[0].id}] selectedTrigger.parameters = [{"value": responseJson[0].displayName, "name": "outlookfolder", "id": responseJson[0].id}] @@ -4956,7 +4961,7 @@ const AngularWorkflow = (props) => { } const getTriggerAuth = () => { - fetch(globalUrl+"/api/v1/triggers/"+selectedTrigger.id, { + fetch(globalUrl+"/api/v1/triggers/outlook/"+selectedTrigger.id, { method: "GET", headers: {"content-type": "application/json"}, credentials: "include", @@ -4987,15 +4992,22 @@ const AngularWorkflow = (props) => { const outlookButton =