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
This commit is contained in:
parent
57b0d9a38b
commit
df2c11a878
5 changed files with 175 additions and 40 deletions
155
models/issue.go
155
models/issue.go
|
@ -1477,46 +1477,18 @@ func getParticipantsByIssueID(e Engine, issueID int64) ([]*User, error) {
|
||||||
return users, e.In("id", userIDs).Find(&users)
|
return users, e.In("id", userIDs).Find(&users)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateIssueMentions extracts mentioned people from content and
|
// UpdateIssueMentions updates issue-user relations for mentioned users.
|
||||||
// updates issue-user relations for them.
|
func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []*User) error {
|
||||||
func UpdateIssueMentions(ctx DBContext, issueID int64, mentions []string) error {
|
|
||||||
if len(mentions) == 0 {
|
if len(mentions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
ids := make([]int64, len(mentions))
|
||||||
for i := range mentions {
|
for i, u := range mentions {
|
||||||
mentions[i] = strings.ToLower(mentions[i])
|
ids[i] = u.ID
|
||||||
}
|
}
|
||||||
users := make([]*User, 0, len(mentions))
|
|
||||||
|
|
||||||
if err := ctx.e.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil {
|
|
||||||
return fmt.Errorf("find mentioned users: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(ctx.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(ctx, issueID, ids); err != nil {
|
if err := UpdateIssueUsersByMentions(ctx, issueID, ids); err != nil {
|
||||||
return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
|
return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1909,3 +1881,120 @@ func (issue *Issue) updateClosedNum(e Engine) (err error) {
|
||||||
}
|
}
|
||||||
return
|
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(ctx DBContext, doer *User, mentions []string) (users []*User, err error) {
|
||||||
|
if len(mentions) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = issue.loadRepo(ctx.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(ctx.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 := ctx.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 := ctx.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 := ctx.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 := ctx.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(ctx.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
|
||||||
|
}
|
||||||
|
|
|
@ -366,3 +366,35 @@ func TestIssue_InsertIssue(t *testing.T) {
|
||||||
testInsertIssue(t, "my issue1", "special issue's comments?")
|
testInsertIssue(t, "my issue1", "special issue's comments?")
|
||||||
testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?")
|
testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(DefaultDBContext(), 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})
|
||||||
|
}
|
||||||
|
|
|
@ -314,7 +314,7 @@ func (t *Team) UnitEnabled(tp UnitType) bool {
|
||||||
|
|
||||||
func (t *Team) unitEnabled(e Engine, tp UnitType) bool {
|
func (t *Team) unitEnabled(e Engine, tp UnitType) bool {
|
||||||
if err := t.getUnits(e); err != nil {
|
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 {
|
for _, unit := range t.Units {
|
||||||
|
|
|
@ -19,11 +19,18 @@ func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue
|
||||||
}
|
}
|
||||||
|
|
||||||
func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType models.ActionType, issue *models.Issue) (err error) {
|
func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType models.ActionType, issue *models.Issue) (err error) {
|
||||||
mentions := markup.FindAllMentions(c.Content)
|
rawMentions := markup.FindAllMentions(c.Content)
|
||||||
if err = models.UpdateIssueMentions(ctx, c.IssueID, mentions); err != nil {
|
userMentions, err := issue.ResolveMentionsByVisibility(ctx, c.Poster, rawMentions)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", c.IssueID, err)
|
||||||
|
}
|
||||||
|
if err = models.UpdateIssueMentions(ctx, c.IssueID, userMentions); err != nil {
|
||||||
return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
|
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 len(c.Content) > 0 {
|
||||||
if err = mailIssueCommentToParticipants(issue, c.Poster, c.Content, c, mentions); err != nil {
|
if err = mailIssueCommentToParticipants(issue, c.Poster, c.Content, c, mentions); err != nil {
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||||
|
|
|
@ -123,11 +123,18 @@ func MailParticipants(issue *models.Issue, doer *models.User, opType models.Acti
|
||||||
}
|
}
|
||||||
|
|
||||||
func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.User, opType models.ActionType) (err error) {
|
func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.User, opType models.ActionType) (err error) {
|
||||||
mentions := markup.FindAllMentions(issue.Content)
|
rawMentions := markup.FindAllMentions(issue.Content)
|
||||||
|
userMentions, err := issue.ResolveMentionsByVisibility(ctx, doer, rawMentions)
|
||||||
if err = models.UpdateIssueMentions(ctx, issue.ID, mentions); err != nil {
|
if err != nil {
|
||||||
|
return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", issue.ID, err)
|
||||||
|
}
|
||||||
|
if err = models.UpdateIssueMentions(ctx, issue.ID, userMentions); err != nil {
|
||||||
return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
|
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 len(issue.Content) > 0 {
|
||||||
if err = mailIssueCommentToParticipants(issue, doer, issue.Content, nil, mentions); err != nil {
|
if err = mailIssueCommentToParticipants(issue, doer, issue.Content, nil, mentions); err != nil {
|
||||||
|
|
Reference in a new issue