Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport: Ignore mentions for users with no access (#8395) #8484

Merged
merged 3 commits into from
Oct 13, 2019
Merged
Show file tree
Hide file tree
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
155 changes: 122 additions & 33 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
13 changes: 10 additions & 3 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 10 additions & 3 deletions models/issue_mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions models/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}
2 changes: 1 addition & 1 deletion models/org_team.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down