]> source.dussan.org Git - gitea.git/commitdiff
Backport: Ignore mentions for users with no access (#8395) (#8484)
authorguillep2k <18600385+guillep2k@users.noreply.github.com>
Sun, 13 Oct 2019 15:17:53 +0000 (12:17 -0300)
committerzeripath <art27@cantab.net>
Sun, 13 Oct 2019 15:17:53 +0000 (16:17 +0100)
* Ignore mentions for users with no access

* Fix fmt

models/issue.go
models/issue_comment.go
models/issue_mail.go
models/issue_test.go
models/org_team.go

index 33e13bb180c55c6734379d9f1e7bfc3bc926c81d..ea2e8d0d92ac948e9f002266c1d273a6ba44d7f9 100644 (file)
@@ -1462,46 +1462,18 @@ func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
        return users, e.In("id", userIDs).Find(&users)
 }
 
-// UpdateIssueMentions extracts mentioned people from content and
-// updates issue-user relations for them.
-func UpdateIssueMentions(e Engine, issueID int64, mentions []string) error {
+// UpdateIssueMentions updates issue-user relations for mentioned users.
+func UpdateIssueMentions(e Engine, issueID int64, mentions []*User) error {
        if len(mentions) == 0 {
                return nil
        }
-
-       for i := range mentions {
-               mentions[i] = strings.ToLower(mentions[i])
-       }
-       users := make([]*User, 0, len(mentions))
-
-       if err := e.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil {
-               return fmt.Errorf("find mentioned users: %v", err)
+       ids := make([]int64, len(mentions))
+       for i, u := range mentions {
+               ids[i] = u.ID
        }
-
-       ids := make([]int64, 0, len(mentions))
-       for _, user := range users {
-               ids = append(ids, user.ID)
-               if !user.IsOrganization() || user.NumMembers == 0 {
-                       continue
-               }
-
-               memberIDs := make([]int64, 0, user.NumMembers)
-               orgUsers, err := getOrgUsersByOrgID(e, user.ID)
-               if err != nil {
-                       return fmt.Errorf("GetOrgUsersByOrgID [%d]: %v", user.ID, err)
-               }
-
-               for _, orgUser := range orgUsers {
-                       memberIDs = append(memberIDs, orgUser.ID)
-               }
-
-               ids = append(ids, memberIDs...)
-       }
-
        if err := UpdateIssueUsersByMentions(e, issueID, ids); err != nil {
                return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
        }
-
        return nil
 }
 
@@ -1854,3 +1826,120 @@ func (issue *Issue) updateClosedNum(e Engine) (err error) {
        }
        return
 }
+
+// ResolveMentionsByVisibility returns the users mentioned in an issue, removing those that
+// don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
+func (issue *Issue) ResolveMentionsByVisibility(e Engine, doer *User, mentions []string) (users []*User, err error) {
+       if len(mentions) == 0 {
+               return
+       }
+       if err = issue.loadRepo(e); err != nil {
+               return
+       }
+       resolved := make(map[string]bool, 20)
+       names := make([]string, 0, 20)
+       resolved[doer.LowerName] = true
+       for _, name := range mentions {
+               name := strings.ToLower(name)
+               if _, ok := resolved[name]; ok {
+                       continue
+               }
+               resolved[name] = false
+               names = append(names, name)
+       }
+
+       if err := issue.Repo.getOwner(e); err != nil {
+               return nil, err
+       }
+
+       if issue.Repo.Owner.IsOrganization() {
+               // Since there can be users with names that match the name of a team,
+               // if the team exists and can read the issue, the team takes precedence.
+               teams := make([]*Team, 0, len(names))
+               if err := e.
+                       Join("INNER", "team_repo", "team_repo.team_id = team.id").
+                       Where("team_repo.repo_id=?", issue.Repo.ID).
+                       In("team.lower_name", names).
+                       Find(&teams); err != nil {
+                       return nil, fmt.Errorf("find mentioned teams: %v", err)
+               }
+               if len(teams) != 0 {
+                       checked := make([]int64, 0, len(teams))
+                       unittype := UnitTypeIssues
+                       if issue.IsPull {
+                               unittype = UnitTypePullRequests
+                       }
+                       for _, team := range teams {
+                               if team.Authorize >= AccessModeOwner {
+                                       checked = append(checked, team.ID)
+                                       resolved[team.LowerName] = true
+                                       continue
+                               }
+                               has, err := e.Get(&TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
+                               if err != nil {
+                                       return nil, fmt.Errorf("get team units (%d): %v", team.ID, err)
+                               }
+                               if has {
+                                       checked = append(checked, team.ID)
+                                       resolved[team.LowerName] = true
+                               }
+                       }
+                       if len(checked) != 0 {
+                               teamusers := make([]*User, 0, 20)
+                               if err := e.
+                                       Join("INNER", "team_user", "team_user.uid = `user`.id").
+                                       In("`team_user`.team_id", checked).
+                                       And("`user`.is_active = ?", true).
+                                       And("`user`.prohibit_login = ?", false).
+                                       Find(&teamusers); err != nil {
+                                       return nil, fmt.Errorf("get teams users: %v", err)
+                               }
+                               if len(teamusers) > 0 {
+                                       users = make([]*User, 0, len(teamusers))
+                                       for _, user := range teamusers {
+                                               if already, ok := resolved[user.LowerName]; !ok || !already {
+                                                       users = append(users, user)
+                                                       resolved[user.LowerName] = true
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               // Remove names already in the list to avoid querying the database if pending names remain
+               names = make([]string, 0, len(resolved))
+               for name, already := range resolved {
+                       if !already {
+                               names = append(names, name)
+                       }
+               }
+               if len(names) == 0 {
+                       return
+               }
+       }
+
+       unchecked := make([]*User, 0, len(names))
+       if err := e.
+               Where("`user`.is_active = ?", true).
+               And("`user`.prohibit_login = ?", false).
+               In("`user`.lower_name", names).
+               Find(&unchecked); err != nil {
+               return nil, fmt.Errorf("find mentioned users: %v", err)
+       }
+       for _, user := range unchecked {
+               if already := resolved[user.LowerName]; already || user.IsOrganization() {
+                       continue
+               }
+               // Normal users must have read access to the referencing issue
+               perm, err := getUserRepoPermission(e, issue.Repo, user)
+               if err != nil {
+                       return nil, fmt.Errorf("getUserRepoPermission [%d]: %v", user.ID, err)
+               }
+               if !perm.CanReadIssuesOrPulls(issue.IsPull) {
+                       continue
+               }
+               users = append(users, user)
+       }
+
+       return
+}
index d5e10fa64506de7e4a7f86d28fa26a2d7b4321d6..0a1d9852cf2fdafb20959d400b327a1f96694a90 100644 (file)
@@ -387,11 +387,18 @@ func (c *Comment) MailParticipants(opType ActionType, issue *Issue) (err error)
 }
 
 func (c *Comment) mailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
-       mentions := markup.FindAllMentions(c.Content)
-       if err = UpdateIssueMentions(e, c.IssueID, mentions); err != nil {
+       rawMentions := markup.FindAllMentions(c.Content)
+       userMentions, err := issue.ResolveMentionsByVisibility(e, c.Poster, rawMentions)
+       if err != nil {
+               return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", c.IssueID, err)
+       }
+       if err = UpdateIssueMentions(e, c.IssueID, userMentions); err != nil {
                return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
        }
-
+       mentions := make([]string, len(userMentions))
+       for i, u := range userMentions {
+               mentions[i] = u.LowerName
+       }
        if len(c.Content) > 0 {
                if err = mailIssueCommentToParticipants(e, issue, c.Poster, c.Content, c, mentions); err != nil {
                        log.Error("mailIssueCommentToParticipants: %v", err)
index 01a12b16d2a8e407293a0fbaf5ff95285007f39f..d429e60629bbb4502c2641b00db02a468acb0cdb 100644 (file)
@@ -123,11 +123,18 @@ func (issue *Issue) MailParticipants(doer *User, opType ActionType) (err error)
 }
 
 func (issue *Issue) mailParticipants(e Engine, doer *User, opType ActionType) (err error) {
-       mentions := markup.FindAllMentions(issue.Content)
-
-       if err = UpdateIssueMentions(e, issue.ID, mentions); err != nil {
+       rawMentions := markup.FindAllMentions(issue.Content)
+       userMentions, err := issue.ResolveMentionsByVisibility(e, doer, rawMentions)
+       if err != nil {
+               return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", issue.ID, err)
+       }
+       if err = UpdateIssueMentions(e, issue.ID, userMentions); err != nil {
                return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
        }
+       mentions := make([]string, len(userMentions))
+       for i, u := range userMentions {
+               mentions[i] = u.LowerName
+       }
 
        if len(issue.Content) > 0 {
                if err = mailIssueCommentToParticipants(e, issue, doer, issue.Content, nil, mentions); err != nil {
index 1a7e45ae02bda279d2b6d1e4e2d8587bf5a12f94..0108504b4b00cd21455d95ea8460625786aeb6d2 100644 (file)
@@ -320,3 +320,35 @@ func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
        assert.EqualValues(t, 1, total)
        assert.EqualValues(t, []int64{1}, ids)
 }
+
+func TestIssue_ResolveMentions(t *testing.T) {
+       assert.NoError(t, PrepareTestDatabase())
+
+       testSuccess := func(owner, repo, doer string, mentions []string, expected []int64) {
+               o := AssertExistsAndLoadBean(t, &User{LowerName: owner}).(*User)
+               r := AssertExistsAndLoadBean(t, &Repository{OwnerID: o.ID, LowerName: repo}).(*Repository)
+               issue := &Issue{RepoID: r.ID}
+               d := AssertExistsAndLoadBean(t, &User{LowerName: doer}).(*User)
+               resolved, err := issue.ResolveMentionsByVisibility(x, d, mentions)
+               assert.NoError(t, err)
+               ids := make([]int64, len(resolved))
+               for i, user := range resolved {
+                       ids[i] = user.ID
+               }
+               sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
+               assert.EqualValues(t, expected, ids)
+       }
+
+       // Public repo, existing user
+       testSuccess("user2", "repo1", "user1", []string{"user5"}, []int64{5})
+       // Public repo, non-existing user
+       testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
+       // Public repo, doer
+       testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
+       // Private repo, team member
+       testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
+       // Private repo, not a team member
+       testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{})
+       // Private repo, whole team
+       testSuccess("user17", "big_test_private_4", "user15", []string{"owners"}, []int64{18})
+}
index 531d235dcd0924b43a306c5bcaa34288a02f0660..371501d2165349c5279ede45ea1e108cc9091cc6 100644 (file)
@@ -252,7 +252,7 @@ func (t *Team) UnitEnabled(tp UnitType) bool {
 
 func (t *Team) unitEnabled(e Engine, tp UnitType) bool {
        if err := t.getUnits(e); err != nil {
-               log.Warn("Error loading repository (ID: %d) units: %s", t.ID, err.Error())
+               log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error())
        }
 
        for _, unit := range t.Units {