From b970917318dd2d85427c42b3504fd8621ea1a315 Mon Sep 17 00:00:00 2001 From: Frikky Date: Wed, 27 Sep 2023 01:40:48 +0200 Subject: [PATCH] Made it possible for orgs to be creators, and admins inside the org to modify them --- cloudSync.go | 103 +++++++++++++++++++++++++++++++++++++++- db-connector.go | 90 +++++++++++++++++++++++++++++++++++ oauth2.go | 2 +- shared.go | 124 ++++++++++++++++++++++++++++++++++++++++++++---- structs.go | 5 +- 5 files changed, 311 insertions(+), 13 deletions(-) diff --git a/cloudSync.go b/cloudSync.go index 8ff4e5c..4645597 100755 --- a/cloudSync.go +++ b/cloudSync.go @@ -2,6 +2,7 @@ package shuffle import ( "bytes" + "net/url" "context" "encoding/json" "errors" @@ -214,6 +215,17 @@ func HandleAlgoliaAppSearchByUser(ctx context.Context, userId string) ([]Algolia } func HandleAlgoliaCreatorSearch(ctx context.Context, username string) (AlgoliaSearchCreator, error) { + tmpUsername, err := url.QueryUnescape(username) + if err == nil { + username = tmpUsername + } + + if strings.HasPrefix(username, "@") { + username = strings.Replace(username, "@", "", 1) + } + + username = strings.ToLower(strings.TrimSpace(username)) + cacheKey := fmt.Sprintf("algolia_creator_%s", username) searchCreator := AlgoliaSearchCreator{} cache, err := GetCache(ctx, cacheKey) @@ -316,7 +328,7 @@ func HandleAlgoliaCreatorSearch(ctx context.Context, username string) (AlgoliaSe return foundUser, nil } -func HandleAlgoliaCreatorUpload(ctx context.Context, user User, overwrite bool) (string, error) { +func HandleAlgoliaCreatorUpload(ctx context.Context, user User, overwrite bool, isOrg bool) (string, error) { algoliaClient := os.Getenv("ALGOLIA_CLIENT") algoliaSecret := os.Getenv("ALGOLIA_SECRET") if len(algoliaClient) == 0 || len(algoliaSecret) == 0 { @@ -359,6 +371,7 @@ func HandleAlgoliaCreatorUpload(ctx context.Context, user User, overwrite bool) TimeEdited: timeNow, Image: user.PublicProfile.GithubAvatar, Username: user.PublicProfile.GithubUsername, + IsOrg: isOrg, }, } @@ -372,6 +385,52 @@ func HandleAlgoliaCreatorUpload(ctx context.Context, user User, overwrite bool) return user.Id, nil } +func HandleAlgoliaCreatorDeletion(ctx context.Context, userId string) (error) { + algoliaClient := os.Getenv("ALGOLIA_CLIENT") + algoliaSecret := os.Getenv("ALGOLIA_SECRET") + if len(algoliaClient) == 0 || len(algoliaSecret) == 0 { + log.Printf("[WARNING] ALGOLIA_CLIENT or ALGOLIA_SECRET not defined") + return errors.New("Algolia keys not defined") + } + + algClient := search.NewClient(algoliaClient, algoliaSecret) + algoliaIndex := algClient.InitIndex("creators") + res, err := algoliaIndex.Search(userId) + if err != nil { + log.Printf("[WARNING] Failed searching Algolia creators: %s", err) + return err + } + + var newRecords []AlgoliaSearchCreator + err = res.UnmarshalHits(&newRecords) + if err != nil { + log.Printf("[WARNING] Failed unmarshaling from Algolia creators: %s", err) + return err + } + + //log.Printf("RECORDS: %d", len(newRecords)) + foundItem := AlgoliaSearchCreator{} + for _, newRecord := range newRecords { + if newRecord.ObjectID == userId { + foundItem = newRecord + break + } + } + + // Should delete it? + if len(foundItem.ObjectID) > 0 { + _, err = algoliaIndex.DeleteObject(foundItem.ObjectID) + if err != nil { + log.Printf("[WARNING] Algolia Creator delete problem: %s", err) + return err + } + + log.Printf("[INFO] Successfully removed creator %s with ID %s FROM ALGOLIA!", foundItem.Username, userId) + } + + return nil +} + // Shitty temorary system // Adding schedule to run over with another algorithm // as well as this one, as to increase priority based on popularity: @@ -675,3 +734,45 @@ func RedirectUserRequest(w http.ResponseWriter, req *http.Request) { DeleteCache(ctx, fmt.Sprintf("session_%s", c.Value)) } } + +// Checks if a specific user should have "self" access to a creator user +// A creator user can be both a user and an org, so this got a bit tricky +func CheckCreatorSelfPermission(ctx context.Context, requestUser, creatorUser User, algoliaUser *AlgoliaSearchCreator) bool { + if project.Environment != "cloud" { + return false + } + + if creatorUser.Id == requestUser.Id { + return true + } else { + for _, user := range algoliaUser.Synonyms { + if user == requestUser.Id { + return true + } + } + + if algoliaUser.IsOrg { + log.Printf("[AUDIT] User %s (%s) is an org. Checking if the current user should have access.", algoliaUser.Username, algoliaUser.ObjectID) + // Get the org and check + org, err := GetOrgByCreatorId(ctx, algoliaUser.ObjectID) + if err != nil { + log.Printf("[WARNING] Couldn't find org for creator %s (%s): %s", algoliaUser.Username, algoliaUser.ObjectID, err) + return false + } + + log.Printf("[AUDIT] Found org %s (%s) for creator %s (%s)", org.Name, org.Id, algoliaUser.Username, algoliaUser.ObjectID) + for _, user := range org.Users { + if user.Id == requestUser.Id { + if user.Role == "admin" { + return true + } + + break + } + } + + } + } + + return false +} diff --git a/db-connector.go b/db-connector.go index 7fcd162..db19b6e 100755 --- a/db-connector.go +++ b/db-connector.go @@ -2606,6 +2606,96 @@ func GetAllWorkflows(ctx context.Context, orgId string) ([]Workflow, error) { return allworkflows, nil } +func GetOrgByCreatorId(ctx context.Context, id string) (*Org, error) { + nameKey := "Organizations" + cacheKey := fmt.Sprintf("creator_%s_%s", nameKey, id) + + curOrg := &Org{} + if project.CacheDb { + cache, err := GetCache(ctx, cacheKey) + if err == nil { + cacheData := []byte(cache.([]uint8)) + //log.Printf("CACHEDATA: %s", cacheData) + err = json.Unmarshal(cacheData, &curOrg) + if err == nil { + return curOrg, nil + } + } else { + //log.Printf("[DEBUG] Failed getting cache for org: %s", err) + } + } + + setOrg := false + if project.DbType == "opensearch" { + } else { + query := datastore.NewQuery(nameKey).Filter("creator_id =", id).Limit(1) + + allOrgs := []Org{} + _, err := project.Dbclient.GetAll(ctx, query, &allOrgs) + if err != nil { + return curOrg, err + } + + if len(allOrgs) > 0 { + curOrg = &allOrgs[0] + } + } + + // How does this happen? + if len(curOrg.Id) == 0 { + curOrg.Id = id + return curOrg, errors.New(fmt.Sprintf("Couldn't find creator org with ID %s", curOrg.Id)) + } + + newUsers := []User{} + for _, user := range curOrg.Users { + user.Password = "" + user.Session = "" + user.ResetReference = "" + user.PrivateApps = []WorkflowApp{} + user.VerificationToken = "" + //user.ApiKey = "" + user.Executions = ExecutionInfo{} + newUsers = append(newUsers, user) + } + + curOrg.Users = newUsers + if len(curOrg.Tutorials) == 0 { + curOrg = GetTutorials(ctx, *curOrg, true) + } + + // Making sure to skip old irrelevant priorities + newPriorities := []Priority{} + for _, priority := range curOrg.Priorities { + if priority.Type == "usecases" { + continue + } + + newPriorities = append(newPriorities, priority) + } + + curOrg.Priorities = newPriorities + if project.CacheDb { + neworg, err := json.Marshal(curOrg) + if err != nil { + log.Printf("[ERROR] Failed marshalling org for cache: %s", err) + return curOrg, nil + } + + err = SetCache(ctx, cacheKey, neworg, 1440) + if err != nil { + log.Printf("[ERROR] Failed updating org cache: %s", err) + } + + if setOrg { + log.Printf("[INFO] UPDATING ORG %s!!", curOrg.Id) + SetOrg(ctx, *curOrg, curOrg.Id) + } + } + + return curOrg, nil +} + // ListBooks returns a list of books, ordered by title. // Handles org grabbing and user / org migrations func GetOrg(ctx context.Context, id string) (*Org, error) { diff --git a/oauth2.go b/oauth2.go index 6ac675c..c0178ae 100755 --- a/oauth2.go +++ b/oauth2.go @@ -470,7 +470,7 @@ func HandleNewGithubRegister(resp http.ResponseWriter, request *http.Request) { return } - _, err = HandleAlgoliaCreatorUpload(ctx, user, false) + _, err = HandleAlgoliaCreatorUpload(ctx, user, false, false) if err != nil { log.Printf("[ERROR] Failed making user %s' information public") } diff --git a/shared.go b/shared.go index 739402b..ec266b9 100755 --- a/shared.go +++ b/shared.go @@ -3435,12 +3435,13 @@ func HandleUpdateUser(resp http.ResponseWriter, request *http.Request) { // NEVER allow the user to set all the data themselves type newUserStruct struct { + UserId string `json:"user_id"` + Tutorial string `json:"tutorial" datastore:"tutorial"` Firstname string `json:"firstname"` Lastname string `json:"lastname"` Role string `json:"role"` Username string `json:"username"` - UserId string `json:"user_id"` EthInfo EthInfo `json:"eth_info"` CompanyRole string `json:"company_role"` Suborgs []string `json:"suborgs"` @@ -3491,10 +3492,17 @@ func HandleUpdateUser(resp http.ResponseWriter, request *http.Request) { } if !orgFound { - log.Printf("[AUDIT] User %s is admin, but can't edit users outside their own org.", userInfo.Id) - resp.WriteHeader(401) - resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't change users outside your org."}`))) - return + isSelf := false + if project.Environment == "cloud" && len(foundUser.Id) == 32 { + isSelf = CheckCreatorSelfPermission(ctx, userInfo, *foundUser, &AlgoliaSearchCreator{ObjectID: foundUser.Id, IsOrg: true,}) + } + + if !isSelf || len(foundUser.Id) != 32 { + log.Printf("[AUDIT] User %s is admin, but can't edit users outside their own org (%s).", userInfo.Id, foundUser.Id) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't change users outside your org."}`))) + return + } } orgUpdater := true @@ -3598,10 +3606,27 @@ func HandleUpdateUser(resp http.ResponseWriter, request *http.Request) { foundUser.EthInfo = t.EthInfo } + // Check if UserID is different? + /* + if len(t.UserId) > 0 && t.UserId != foundUser.Id { + log.Printf("[DEBUG] Should set userid to %s", t.UserId) + newUser, err := GetUser(ctx, t.UserId) + if err != nil { + log.Printf("[WARNING] Failed getting user %s: %s", t.UserId, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Username and/or password is incorrect"}`)) + return + } + + log.Printf("[DEBUG] Found the user with username %s", newUser.Username) + } + */ + + log.Printf("[DEBUG] Found github username %s. ID to look for: %s", foundUser.PublicProfile.GithubUsername, t.UserId) + username := foundUser.PublicProfile.GithubUsername creator, err := HandleAlgoliaCreatorSearch(ctx, username) - // FIXME: If any of the parts below are updated, also update // the same field within Algolia itself. if err == nil { // Related to creators @@ -3703,10 +3728,6 @@ func HandleUpdateUser(resp http.ResponseWriter, request *http.Request) { } t.Suborgs = newSuborgs - //log.Printf("[DEBUG] Valid suborgs: %s", t.Suborgs) - - // 244199a7-0009-44af-aefb-55da92334583 - // 583816e5-40ab-4212-8c7a-e54c8edd6b74 addedOrgs := []string{} for _, suborg := range t.Suborgs { @@ -6738,6 +6759,8 @@ func DeleteUser(resp http.ResponseWriter, request *http.Request) { } if !orgFound { + + log.Printf("[AUDIT] User %s is admin, but can't delete users outside their own org.", userInfo.Id) resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Can't change users outside your org."}`))) @@ -7793,6 +7816,8 @@ func HandleEditOrg(resp http.ResponseWriter, request *http.Request) { Defaults Defaults `json:"defaults" datastore:"defaults"` SSOConfig SSOConfig `json:"sso_config" datastore:"sso_config"` LeadInfo []string `json:"lead_info" datastore:"lead_info"` + + CreatorConfig string `json:"creator_config" datastore:"creator_config"` } var tmpData ReturnData @@ -7987,6 +8012,85 @@ func HandleEditOrg(resp http.ResponseWriter, request *http.Request) { org.LeadInfo = newLeadinfo } + if len(tmpData.CreatorConfig) > 0 { + // Check if they're a creator already + if tmpData.CreatorConfig == "join" { + + if org.CreatorId != "" { + log.Printf("[WARNING] Org %s is already a creator", org.Id) + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false}`)) + return + } + + // Make md5 from current ID (to make it replicable) + hasher := md5.New() + hasher.Write([]byte(org.Id)) + creatorId := hex.EncodeToString(hasher.Sum(nil)) + + log.Printf("[INFO] Org %s (%s) is joining creators with ID %s", org.Name, org.Id, creatorId) + + org.CreatorId = creatorId + parsedCreatorUser := User{ + Id: creatorId, + Username: org.Name, + } + + parsedCreatorUser.PublicProfile.GithubAvatar = org.Image + parsedCreatorUser.PublicProfile.GithubUsername = org.Name + + HandleAlgoliaCreatorUpload(ctx, parsedCreatorUser, false, true) + + // Should create a new user with the same ID as the org creatorId + // This is to save public information about the org, which is used for verifying access in all other APIs + // In short: It's a way to NOT have to make all old Creator API's also support orgs. A hack, but it works + + // Try to get the user + foundUser, err := GetUser(ctx, creatorId) + if err != nil { + log.Printf("[WARNING] Failed to get creator user %s: %s", creatorId, err) + + // Create the user + creatorUser := parsedCreatorUser + + creatorUser.PublicProfile.Public = true + creatorUser.PublicProfile.GithubUsername = org.Name + creatorUser.PublicProfile.GithubUserid = org.CreatorId + creatorUser.PublicProfile.GithubAvatar = org.Image + + SetUser(ctx, &creatorUser, false) + + } else { + log.Printf("[INFO] Creator user %s already exists. Should set back to public.", creatorId) + + foundUser.PublicProfile.Public = true + SetUser(ctx, foundUser, false) + + } + + } else if tmpData.CreatorConfig == "leave" { + log.Printf("[INFO] Org %s is leaving creators", org.Id) + if org.CreatorId != "" { + // Remove item with the ID from Algolia + + foundUser, err := GetUser(ctx, org.CreatorId) + if err == nil { + foundUser.PublicProfile.Public = false + + SetUser(ctx, foundUser, false) + } + + err = HandleAlgoliaCreatorDeletion(ctx, org.CreatorId) + if err != nil { + log.Printf("[WARNING] Failed to remove creator %s (%s) from Algolia: %s", org.Name, org.CreatorId, err) + } else { + org.CreatorId = "" + } + } + + } + } + // Built a system around this now, which checks for the actual org. // if requestdata.Environment == "cloud" && project.Environment != "cloud" { //if project.Environment != "cloud" && len(org.SSOConfig.SSOEntrypoint) > 0 && len(org.ManagerOrgs) == 0 { diff --git a/structs.go b/structs.go index 1e2a3a3..2af79d7 100755 --- a/structs.go +++ b/structs.go @@ -419,7 +419,7 @@ type PublicProfile struct { Self bool `datastore:"self" json:"self"` GithubUsername string `datastore:"github_username" json:"github_username"` GithubUserid string `datastore:"github_userid" json:"github_userid"` - GithubAvatar string `datastore:"github_avatar" json:"github_avatar"` + GithubAvatar string `datastore:"github_avatar,noindex" json:"github_avatar"` GithubLocation string `datastore:"github_location" json:"github_location"` GithubUrl string `datastore:"github_url" json:"github_url"` GithubBio string `datastore:"github_bio" json:"github_bio"` @@ -771,6 +771,8 @@ type Org struct { RegionUrl string `json:"region_url" datastore:"region_url"` Tutorials []Tutorial `json:"tutorials" datastore:"tutorials"` LeadInfo LeadInfo `json:"lead_info,omitempty" datastore:"lead_info"` + + CreatorId string `json:"creator_id" datastore:"creator_id"` } type PartnerInfo struct { @@ -1273,6 +1275,7 @@ type AlgoliaSearchCreator struct { Social []string `datastore:"social" json:"social"` WorkStatus string `datastore:"work_status" json:"work_status"` Url string `datastore:"url" json:"url"` + IsOrg bool `datastore:"is_org" json:"is_org"` } type AlgoliaSearchWorkflow struct {