From b4a1e5001340a98fbe552b93e4e3fad484a4db2f Mon Sep 17 00:00:00 2001 From: Lalit Deore Date: Tue, 14 Apr 2026 18:20:23 +0530 Subject: [PATCH 1/2] add feature of promoting suborg to parent org --- shared.go | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) diff --git a/shared.go b/shared.go index af1850ee..002bfea3 100644 --- a/shared.go +++ b/shared.go @@ -13165,6 +13165,257 @@ func HandleCreateSubOrg(resp http.ResponseWriter, request *http.Request) { } +func resolveParentOrgId(subOrg *Org) string { + if subOrg == nil { + return "" + } + + if subOrg.CreatorOrg != "" { + return subOrg.CreatorOrg + } + + if len(subOrg.ManagerOrgs) > 0 { + return subOrg.ManagerOrgs[0].Id + } + + return "" +} + +func clearPromoteSubOrgCaches(ctx context.Context, parentOrgId, subOrgId string, parentOrg *Org, subOrg *Org) { + DeleteCache(ctx, fmt.Sprintf("Organizations_%s", parentOrgId)) + DeleteCache(ctx, fmt.Sprintf("Organizations_%s", subOrgId)) + DeleteCache(ctx, fmt.Sprintf("%s__childorgs", parentOrgId)) + DeleteCache(ctx, fmt.Sprintf("%s_childorgs", parentOrgId)) + DeleteCache(ctx, fmt.Sprintf("creator_Organizations_%s", subOrgId)) + DeleteCache(ctx, fmt.Sprintf("apps_%s", parentOrgId)) + DeleteCache(ctx, fmt.Sprintf("apps_%s", subOrgId)) + DeleteCache(ctx, fmt.Sprintf("%s_workflows", parentOrgId)) + DeleteCache(ctx, fmt.Sprintf("%s_workflows", subOrgId)) + DeleteCache(ctx, fmt.Sprintf("files_%s_", parentOrgId)) + DeleteCache(ctx, fmt.Sprintf("files_%s_", subOrgId)) + DeleteCache(ctx, fmt.Sprintf("org_cache_%s_%s_%s", "", parentOrgId, "")) + DeleteCache(ctx, fmt.Sprintf("org_cache_%s_%s_%s", "", subOrgId, "")) + DeleteCache(ctx, fmt.Sprintf("datastore_category_%s", parentOrgId)) + DeleteCache(ctx, fmt.Sprintf("datastore_category_%s", subOrgId)) + DeleteCache(ctx, fmt.Sprintf("workflowappauth_%s", subOrgId)) + DeleteCache(ctx, fmt.Sprintf("workflowappauth_%s", parentOrgId)) + DeleteCache(ctx, fmt.Sprintf("Environments_%s", subOrgId)) + DeleteCache(ctx, fmt.Sprintf("Environments_%s", parentOrgId)) + + processedUsers := map[string]bool{} + allUsers := []User{} + if parentOrg != nil { + allUsers = append(allUsers, parentOrg.Users...) + } + if subOrg != nil { + allUsers = append(allUsers, subOrg.Users...) + } + + for _, orgUser := range allUsers { + if processedUsers[orgUser.Id] { + continue + } + processedUsers[orgUser.Id] = true + + DeleteCache(ctx, fmt.Sprintf("user_%s", orgUser.Id)) + DeleteCache(ctx, fmt.Sprintf("user_%s", orgUser.Username)) + DeleteCache(ctx, fmt.Sprintf("apps_%s", orgUser.Id)) + DeleteCache(ctx, fmt.Sprintf("%s_workflows", orgUser.Id)) + DeleteCache(ctx, fmt.Sprintf("user_orgs_%s", orgUser.Id)) + DeleteCache(ctx, fmt.Sprintf("Users_%s", orgUser.ApiKey)) + DeleteCache(ctx, fmt.Sprintf("%s", orgUser.ApiKey)) + DeleteCache(ctx, orgUser.Session) + DeleteCache(ctx, fmt.Sprintf("session_%s", orgUser.Session)) + } +} + +func HandlePromoteSubOrg(resp http.ResponseWriter, request *http.Request) { + cors := HandleCors(resp, request) + if cors { + return + } + + user, err := HandleApiAuthentication(resp, request) + if err != nil { + log.Printf("[WARNING] Api authentication failed in promote suborg: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Authentication failed"}`)) + return + } + + var parentOrgId string + location := strings.Split(request.URL.String(), "/") + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Bad request path"}`)) + return + } + + parentOrgId = location[4] + } + + if strings.Contains(parentOrgId, "?") { + parentOrgId = strings.Split(parentOrgId, "?")[0] + } + + type promoteRequest struct { + OrgId string `json:"org_id"` + SuborgId string `json:"suborg_id"` + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "Failed reading body"}`)) + return + } + + requestData := promoteRequest{} + if err := json.Unmarshal(body, &requestData); err != nil { + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "Invalid request body"}`)) + return + } + + if requestData.OrgId != "" && requestData.OrgId != parentOrgId { + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "org_id mismatch between path and body"}`)) + return + } + + if requestData.SuborgId == "" { + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "suborg_id is required"}`)) + return + } + + if !user.SupportAccess { + log.Printf("[WARNING] User %s (%s) attempted to promote suborg without support access", user.Username, user.Id) + resp.WriteHeader(403) + resp.Write([]byte(`{"success": false, "reason": "Support access required"}`)) + return + } + + if project.Environment == "cloud" { + gceProject := os.Getenv("SHUFFLE_GCEPROJECT") + if gceProject != "shuffler" && gceProject != sandboxProject && len(gceProject) > 0 { + ctx := GetContext(request) + parentOrg, _ := GetOrg(ctx, parentOrgId) + subOrg, _ := GetOrg(ctx, requestData.SuborgId) + clearPromoteSubOrgCaches(ctx, parentOrgId, requestData.SuborgId, parentOrg, subOrg) + + log.Printf("[DEBUG] Redirecting Promote Suborg request to main site handler (shuffler.io)") + RedirectUserRequest(resp, request) + return + } + } + + ctx := GetContext(request) + parentOrg, err := GetOrg(ctx, parentOrgId) + if err != nil { + log.Printf("[WARNING] Failed loading parent org '%s' in promote suborg: %s", parentOrgId, err) + resp.WriteHeader(404) + resp.Write([]byte(`{"success": false, "reason": "Parent organization not found"}`)) + return + } + + subOrg, err := GetOrg(ctx, requestData.SuborgId) + if err != nil { + log.Printf("[WARNING] Failed loading suborg '%s' in promote suborg: %s", requestData.SuborgId, err) + resp.WriteHeader(404) + resp.Write([]byte(`{"success": false, "reason": "Sub-organization not found"}`)) + return + } + + if len(parentOrg.CreatorOrg) > 0 { + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "Cannot promote from a sub-organization context"}`)) + return + } + + resolvedParentId := resolveParentOrgId(subOrg) + if resolvedParentId == "" { + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "Sub-organization is already standalone"}`)) + return + } + + if resolvedParentId != parentOrg.Id { + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "Sub-organization does not belong to this parent"}`)) + return + } + + if len(subOrg.ChildOrgs) > 0 { + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "Cannot promote a sub-organization that has child organizations"}`)) + return + } + + subOrg.CreatorOrg = "" + subOrg.ManagerOrgs = []OrgMini{} + + newChildOrgs := []OrgMini{} + removedSuborg := false + for _, child := range parentOrg.ChildOrgs { + if child.Id == subOrg.Id { + removedSuborg = true + continue + } + + newChildOrgs = append(newChildOrgs, child) + } + parentOrg.ChildOrgs = newChildOrgs + subOrg.Description = "" + + if err := SetOrg(ctx, *subOrg, subOrg.Id); err != nil { + log.Printf("[ERROR] Failed writing promoted org '%s': %s", subOrg.Id, err) + resp.WriteHeader(500) + resp.Write([]byte(`{"success": false, "reason": "Failed updating sub-organization"}`)) + return + } + + if err := SetOrg(ctx, *parentOrg, parentOrg.Id); err != nil { + log.Printf("[ERROR] Failed writing parent org '%s' while promoting '%s': %s", parentOrg.Id, subOrg.Id, err) + resp.WriteHeader(500) + resp.Write([]byte(`{"success": false, "reason": "Sub-organization promoted, but parent update failed"}`)) + return + } + + updatedWorkflows := 0 + subOrgUser := User{ + ActiveOrg: OrgMini{Id: subOrg.Id}, + Role: "admin", + } + + workflowItems, err := GetAllWorkflowsByQuery(ctx, subOrgUser, 250, "") + if err != nil { + log.Printf("[WARNING] Failed loading workflows for promoted org '%s': %s", subOrg.Id, err) + } + + for _, workflow := range workflowItems { + if workflow.ParentWorkflowId == "" { + continue + } + + workflow.ParentWorkflowId = "" + if err := SetWorkflow(ctx, workflow, workflow.ID); err != nil { + log.Printf("[WARNING] Failed updating workflow '%s' while promoting '%s': %s", workflow.ID, subOrg.Id, err) + continue + } + + updatedWorkflows += 1 + } + + clearPromoteSubOrgCaches(ctx, parentOrg.Id, subOrg.Id, parentOrg, subOrg) + + log.Printf("[AUDIT] User %s (%s) promoted suborg %s (%s). Parent updated=%t. Workflows=%d", user.Username, user.Id, subOrg.Name, subOrg.Id, removedSuborg, updatedWorkflows) + + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Sub-organization promoted successfully", "removed_from_parent": %t, "updated_workflows": %d}`, removedSuborg, updatedWorkflows))) +} + func getSignatureSample(org Org) PaymentSubscription { if len(org.Subscriptions) > 0 { for _, sub := range org.Subscriptions { From eedb9cc04e97f73248285b2fe29b44cb0194e713 Mon Sep 17 00:00:00 2001 From: Lalit Deore Date: Wed, 15 Apr 2026 14:00:05 +0530 Subject: [PATCH 2/2] use active org and update parent org first then suborg --- shared.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/shared.go b/shared.go index 002bfea3..592a1bd4 100644 --- a/shared.go +++ b/shared.go @@ -13278,7 +13278,7 @@ func HandlePromoteSubOrg(resp http.ResponseWriter, request *http.Request) { return } - if requestData.OrgId != "" && requestData.OrgId != parentOrgId { + if user.ActiveOrg.Id != parentOrgId { resp.WriteHeader(400) resp.Write([]byte(`{"success": false, "reason": "org_id mismatch between path and body"}`)) return @@ -13369,17 +13369,17 @@ func HandlePromoteSubOrg(resp http.ResponseWriter, request *http.Request) { parentOrg.ChildOrgs = newChildOrgs subOrg.Description = "" - if err := SetOrg(ctx, *subOrg, subOrg.Id); err != nil { - log.Printf("[ERROR] Failed writing promoted org '%s': %s", subOrg.Id, err) + if err := SetOrg(ctx, *parentOrg, parentOrg.Id); err != nil { + log.Printf("[ERROR] Failed writing parent org '%s' while promoting '%s': %s", parentOrg.Id, subOrg.Id, err) resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Failed updating sub-organization"}`)) + resp.Write([]byte(`{"success": false, "reason": "Sub-organization promoted, but parent update failed"}`)) return } - if err := SetOrg(ctx, *parentOrg, parentOrg.Id); err != nil { - log.Printf("[ERROR] Failed writing parent org '%s' while promoting '%s': %s", parentOrg.Id, subOrg.Id, err) + if err := SetOrg(ctx, *subOrg, subOrg.Id); err != nil { + log.Printf("[ERROR] Failed writing promoted org '%s': %s", subOrg.Id, err) resp.WriteHeader(500) - resp.Write([]byte(`{"success": false, "reason": "Sub-organization promoted, but parent update failed"}`)) + resp.Write([]byte(`{"success": false, "reason": "Failed updating sub-organization"}`)) return }