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 { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue