Skip to content

Commit df2c11a

Browse files
guillep2klafriks
authored andcommitted
Ignore mentions for users with no access (#8395)
* Draft for ResolveMentionsByVisibility() * Correct typo * Resolve teams instead of orgs for mentions * Create test for ResolveMentionsByVisibility * Fix check for individual users and doer * Test and fix team mentions * Run all mentions through visibility filter * Fix error check * Simplify code, fix doer included in teams * Simplify team id list build
1 parent 57b0d9a commit df2c11a

File tree

5 files changed

+175
-40
lines changed

5 files changed

+175
-40
lines changed

models/issue.go

+122-33
Original file line numberDiff line numberDiff line change
@@ -1477,46 +1477,18 @@ func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
14771477
return users, e.In("id", userIDs).Find(&users)
14781478
}
14791479

1480-
// UpdateIssueMentions extracts mentioned people from content and
1481-
// updates issue-user relations for them.
1482-
func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []string) error {
1480+
// UpdateIssueMentions updates issue-user relations for mentioned users.
1481+
func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []*User) error {
14831482
if len(mentions) == 0 {
14841483
return nil
14851484
}
1486-
1487-
for i := range mentions {
1488-
mentions[i] = strings.ToLower(mentions[i])
1489-
}
1490-
users := make([]*User, 0, len(mentions))
1491-
1492-
if err := ctx.e.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil {
1493-
return fmt.Errorf("find mentioned users: %v", err)
1494-
}
1495-
1496-
ids := make([]int64, 0, len(mentions))
1497-
for _, user := range users {
1498-
ids = append(ids, user.ID)
1499-
if !user.IsOrganization() || user.NumMembers == 0 {
1500-
continue
1501-
}
1502-
1503-
memberIDs := make([]int64, 0, user.NumMembers)
1504-
orgUsers, err := getOrgUsersByOrgID(ctx.e, user.ID)
1505-
if err != nil {
1506-
return fmt.Errorf("GetOrgUsersByOrgID [%d]: %v", user.ID, err)
1507-
}
1508-
1509-
for _, orgUser := range orgUsers {
1510-
memberIDs = append(memberIDs, orgUser.ID)
1511-
}
1512-
1513-
ids = append(ids, memberIDs...)
1485+
ids := make([]int64, len(mentions))
1486+
for i, u := range mentions {
1487+
ids[i] = u.ID
15141488
}
1515-
15161489
if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil {
15171490
return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
15181491
}
1519-
15201492
return nil
15211493
}
15221494

@@ -1909,3 +1881,120 @@ func (issue *Issue) updateClosedNum(e Engine) (err error) {
19091881
}
19101882
return
19111883
}
1884+
1885+
// ResolveMentionsByVisibility returns the users mentioned in an issue, removing those that
1886+
// don't have access to reading it. Teams are expanded into their users, but organizations are ignored.
1887+
func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, mentions []string) (users []*User, err error) {
1888+
if len(mentions) == 0 {
1889+
return
1890+
}
1891+
if err = issue.loadRepo(ctx.e); err != nil {
1892+
return
1893+
}
1894+
resolved := make(map[string]bool, 20)
1895+
names := make([]string, 0, 20)
1896+
resolved[doer.LowerName] = true
1897+
for _, name := range mentions {
1898+
name := strings.ToLower(name)
1899+
if _, ok := resolved[name]; ok {
1900+
continue
1901+
}
1902+
resolved[name] = false
1903+
names = append(names, name)
1904+
}
1905+
1906+
if err := issue.Repo.getOwner(ctx.e); err != nil {
1907+
return nil, err
1908+
}
1909+
1910+
if issue.Repo.Owner.IsOrganization() {
1911+
// Since there can be users with names that match the name of a team,
1912+
// if the team exists and can read the issue, the team takes precedence.
1913+
teams := make([]*Team, 0, len(names))
1914+
if err := ctx.e.
1915+
Join("INNER", "team_repo", "team_repo.team_id = team.id").
1916+
Where("team_repo.repo_id=?", issue.Repo.ID).
1917+
In("team.lower_name", names).
1918+
Find(&teams); err != nil {
1919+
return nil, fmt.Errorf("find mentioned teams: %v", err)
1920+
}
1921+
if len(teams) != 0 {
1922+
checked := make([]int64, 0, len(teams))
1923+
unittype := UnitTypeIssues
1924+
if issue.IsPull {
1925+
unittype = UnitTypePullRequests
1926+
}
1927+
for _, team := range teams {
1928+
if team.Authorize >= AccessModeOwner {
1929+
checked = append(checked, team.ID)
1930+
resolved[team.LowerName] = true
1931+
continue
1932+
}
1933+
has, err := ctx.e.Get(&TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
1934+
if err != nil {
1935+
return nil, fmt.Errorf("get team units (%d): %v", team.ID, err)
1936+
}
1937+
if has {
1938+
checked = append(checked, team.ID)
1939+
resolved[team.LowerName] = true
1940+
}
1941+
}
1942+
if len(checked) != 0 {
1943+
teamusers := make([]*User, 0, 20)
1944+
if err := ctx.e.
1945+
Join("INNER", "team_user", "team_user.uid = `user`.id").
1946+
In("`team_user`.team_id", checked).
1947+
And("`user`.is_active = ?", true).
1948+
And("`user`.prohibit_login = ?", false).
1949+
Find(&teamusers); err != nil {
1950+
return nil, fmt.Errorf("get teams users: %v", err)
1951+
}
1952+
if len(teamusers) > 0 {
1953+
users = make([]*User, 0, len(teamusers))
1954+
for _, user := range teamusers {
1955+
if already, ok := resolved[user.LowerName]; !ok || !already {
1956+
users = append(users, user)
1957+
resolved[user.LowerName] = true
1958+
}
1959+
}
1960+
}
1961+
}
1962+
}
1963+
1964+
// Remove names already in the list to avoid querying the database if pending names remain
1965+
names = make([]string, 0, len(resolved))
1966+
for name, already := range resolved {
1967+
if !already {
1968+
names = append(names, name)
1969+
}
1970+
}
1971+
if len(names) == 0 {
1972+
return
1973+
}
1974+
}
1975+
1976+
unchecked := make([]*User, 0, len(names))
1977+
if err := ctx.e.
1978+
Where("`user`.is_active = ?", true).
1979+
And("`user`.prohibit_login = ?", false).
1980+
In("`user`.lower_name", names).
1981+
Find(&unchecked); err != nil {
1982+
return nil, fmt.Errorf("find mentioned users: %v", err)
1983+
}
1984+
for _, user := range unchecked {
1985+
if already := resolved[user.LowerName]; already || user.IsOrganization() {
1986+
continue
1987+
}
1988+
// Normal users must have read access to the referencing issue
1989+
perm, err := getUserRepoPermission(ctx.e, issue.Repo, user)
1990+
if err != nil {
1991+
return nil, fmt.Errorf("getUserRepoPermission [%d]: %v", user.ID, err)
1992+
}
1993+
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
1994+
continue
1995+
}
1996+
users = append(users, user)
1997+
}
1998+
1999+
return
2000+
}

models/issue_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,35 @@ func TestIssue_InsertIssue(t *testing.T) {
366366
testInsertIssue(t, "my issue1", "special issue's comments?")
367367
testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?")
368368
}
369+
370+
func TestIssue_ResolveMentions(t *testing.T) {
371+
assert.NoError(t, PrepareTestDatabase())
372+
373+
testSuccess := func(owner, repo, doer string, mentions []string, expected []int64) {
374+
o := AssertExistsAndLoadBean(t, &User{LowerName: owner}).(*User)
375+
r := AssertExistsAndLoadBean(t, &Repository{OwnerID: o.ID, LowerName: repo}).(*Repository)
376+
issue := &Issue{RepoID: r.ID}
377+
d := AssertExistsAndLoadBean(t, &User{LowerName: doer}).(*User)
378+
resolved, err := issue.ResolveMentionsByVisibility(DefaultDBContext(), d, mentions)
379+
assert.NoError(t, err)
380+
ids := make([]int64, len(resolved))
381+
for i, user := range resolved {
382+
ids[i] = user.ID
383+
}
384+
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
385+
assert.EqualValues(t, expected, ids)
386+
}
387+
388+
// Public repo, existing user
389+
testSuccess("user2", "repo1", "user1", []string{"user5"}, []int64{5})
390+
// Public repo, non-existing user
391+
testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
392+
// Public repo, doer
393+
testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
394+
// Private repo, team member
395+
testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
396+
// Private repo, not a team member
397+
testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{})
398+
// Private repo, whole team
399+
testSuccess("user17", "big_test_private_4", "user15", []string{"owners"}, []int64{18})
400+
}

models/org_team.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ func (t *Team) UnitEnabled(tp UnitType) bool {
314314

315315
func (t *Team) unitEnabled(e Engine, tp UnitType) bool {
316316
if err := t.getUnits(e); err != nil {
317-
log.Warn("Error loading repository (ID: %d) units: %s", t.ID, err.Error())
317+
log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error())
318318
}
319319

320320
for _, unit := range t.Units {

services/mailer/mail_comment.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,18 @@ func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue
1919
}
2020

2121
func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType models.ActionType, issue *models.Issue) (err error) {
22-
mentions := markup.FindAllMentions(c.Content)
23-
if err = models.UpdateIssueMentions(ctx, c.IssueID, mentions); err != nil {
22+
rawMentions := markup.FindAllMentions(c.Content)
23+
userMentions, err := issue.ResolveMentionsByVisibility(ctx, c.Poster, rawMentions)
24+
if err != nil {
25+
return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", c.IssueID, err)
26+
}
27+
if err = models.UpdateIssueMentions(ctx, c.IssueID, userMentions); err != nil {
2428
return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
2529
}
26-
30+
mentions := make([]string, len(userMentions))
31+
for i, u := range userMentions {
32+
mentions[i] = u.LowerName
33+
}
2734
if len(c.Content) > 0 {
2835
if err = mailIssueCommentToParticipants(issue, c.Poster, c.Content, c, mentions); err != nil {
2936
log.Error("mailIssueCommentToParticipants: %v", err)

services/mailer/mail_issue.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,18 @@ func MailParticipants(issue *models.Issue, doer *models.User, opType models.Acti
123123
}
124124

125125
func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.User, opType models.ActionType) (err error) {
126-
mentions := markup.FindAllMentions(issue.Content)
127-
128-
if err = models.UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
126+
rawMentions := markup.FindAllMentions(issue.Content)
127+
userMentions, err := issue.ResolveMentionsByVisibility(ctx, doer, rawMentions)
128+
if err != nil {
129+
return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", issue.ID, err)
130+
}
131+
if err = models.UpdateIssueMentions(ctx, issue.ID, userMentions); err != nil {
129132
return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
130133
}
134+
mentions := make([]string, len(userMentions))
135+
for i, u := range userMentions {
136+
mentions[i] = u.LowerName
137+
}
131138

132139
if len(issue.Content) > 0 {
133140
if err = mailIssueCommentToParticipants(issue, doer, issue.Content, nil, mentions); err != nil {

0 commit comments

Comments
 (0)