Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document the API right away on the "API" docs. Otherwise we will forget it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(this is why I try to not make new APIs if we don't NEED to) :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, will write doc for it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added doc for it in below PR.

Shuffle/shuffle-docs#227

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 user.ActiveOrg.Id != 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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm primarily worried what would happen here if we give this to users, which is.. kind of the point. They should control this themselves.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this available to normal users? I am afraid that if someone promotes a sub-organization to a parent organization without understanding the impact, they could lose access to distributed auth, files, datastore, and environment data due to how things are currently set up and this can impact workflows. I would prefer to keep this limited to support users. Let me know in case you want me to make it public for normal users.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can just move it back then? References aren't lost..

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, *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
}

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
}

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 == "" {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be the same as "stealing" a workflow?

Not sure.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We skip workflows where workflow.ParentWorkflowId == "".

If a workflow has a parent org ID, it means the workflow is distributed, so we reset it to empty but we are making copy of workflow when distributed. That's why it is still accessible after promoting to parent org.

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 {
Expand Down
Loading