code.gitea.io/gitea@v1.22.3/services/issue/assignee.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package issue 5 6 import ( 7 "context" 8 9 issues_model "code.gitea.io/gitea/models/issues" 10 "code.gitea.io/gitea/models/organization" 11 "code.gitea.io/gitea/models/perm" 12 access_model "code.gitea.io/gitea/models/perm/access" 13 repo_model "code.gitea.io/gitea/models/repo" 14 "code.gitea.io/gitea/models/unit" 15 user_model "code.gitea.io/gitea/models/user" 16 "code.gitea.io/gitea/modules/log" 17 notify_service "code.gitea.io/gitea/services/notify" 18 ) 19 20 // DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array 21 func DeleteNotPassedAssignee(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assignees []*user_model.User) (err error) { 22 var found bool 23 oriAssignes := make([]*user_model.User, len(issue.Assignees)) 24 _ = copy(oriAssignes, issue.Assignees) 25 26 for _, assignee := range oriAssignes { 27 found = false 28 for _, alreadyAssignee := range assignees { 29 if assignee.ID == alreadyAssignee.ID { 30 found = true 31 break 32 } 33 } 34 35 if !found { 36 // This function also does comments and hooks, which is why we call it separately instead of directly removing the assignees here 37 if _, _, err := ToggleAssigneeWithNotify(ctx, issue, doer, assignee.ID); err != nil { 38 return err 39 } 40 } 41 } 42 43 return nil 44 } 45 46 // ToggleAssigneeWithNoNotify changes a user between assigned and not assigned for this issue, and make issue comment for it. 47 func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *issues_model.Comment, err error) { 48 removed, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID) 49 if err != nil { 50 return false, nil, err 51 } 52 53 assignee, err := user_model.GetUserByID(ctx, assigneeID) 54 if err != nil { 55 return false, nil, err 56 } 57 58 notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, removed, comment) 59 60 return removed, comment, err 61 } 62 63 // ReviewRequest add or remove a review request from a user for this PR, and make comment for it. 64 func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) { 65 if isAdd { 66 comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer) 67 } else { 68 comment, err = issues_model.RemoveReviewRequest(ctx, issue, reviewer, doer) 69 } 70 71 if err != nil { 72 return nil, err 73 } 74 75 if comment != nil { 76 notify_service.PullRequestReviewRequest(ctx, doer, issue, reviewer, isAdd, comment) 77 } 78 79 return comment, err 80 } 81 82 // IsValidReviewRequest Check permission for ReviewRequest 83 func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error { 84 if reviewer.IsOrganization() { 85 return issues_model.ErrNotValidReviewRequest{ 86 Reason: "Organization can't be added as reviewer", 87 UserID: doer.ID, 88 RepoID: issue.Repo.ID, 89 } 90 } 91 if doer.IsOrganization() { 92 return issues_model.ErrNotValidReviewRequest{ 93 Reason: "Organization can't be doer to add reviewer", 94 UserID: doer.ID, 95 RepoID: issue.Repo.ID, 96 } 97 } 98 99 permReviewer, err := access_model.GetUserRepoPermission(ctx, issue.Repo, reviewer) 100 if err != nil { 101 return err 102 } 103 104 if permDoer == nil { 105 permDoer = new(access_model.Permission) 106 *permDoer, err = access_model.GetUserRepoPermission(ctx, issue.Repo, doer) 107 if err != nil { 108 return err 109 } 110 } 111 112 lastreview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) 113 if err != nil && !issues_model.IsErrReviewNotExist(err) { 114 return err 115 } 116 117 canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue) 118 119 if isAdd { 120 if !permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests) { 121 return issues_model.ErrNotValidReviewRequest{ 122 Reason: "Reviewer can't read", 123 UserID: doer.ID, 124 RepoID: issue.Repo.ID, 125 } 126 } 127 128 if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 { 129 return issues_model.ErrNotValidReviewRequest{ 130 Reason: "poster of pr can't be reviewer", 131 UserID: doer.ID, 132 RepoID: issue.Repo.ID, 133 } 134 } 135 136 if canDoerChangeReviewRequests { 137 return nil 138 } 139 140 if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest { 141 return nil 142 } 143 144 return issues_model.ErrNotValidReviewRequest{ 145 Reason: "Doer can't choose reviewer", 146 UserID: doer.ID, 147 RepoID: issue.Repo.ID, 148 } 149 } 150 151 if canDoerChangeReviewRequests { 152 return nil 153 } 154 155 if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID { 156 return nil 157 } 158 159 return issues_model.ErrNotValidReviewRequest{ 160 Reason: "Doer can't remove reviewer", 161 UserID: doer.ID, 162 RepoID: issue.Repo.ID, 163 } 164 } 165 166 // IsValidTeamReviewRequest Check permission for ReviewRequest Team 167 func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error { 168 if doer.IsOrganization() { 169 return issues_model.ErrNotValidReviewRequest{ 170 Reason: "Organization can't be doer to add reviewer", 171 UserID: doer.ID, 172 RepoID: issue.Repo.ID, 173 } 174 } 175 176 canDoerChangeReviewRequests := CanDoerChangeReviewRequests(ctx, doer, issue.Repo, issue) 177 178 if isAdd { 179 if issue.Repo.IsPrivate { 180 hasTeam := organization.HasTeamRepo(ctx, reviewer.OrgID, reviewer.ID, issue.RepoID) 181 182 if !hasTeam { 183 return issues_model.ErrNotValidReviewRequest{ 184 Reason: "Reviewing team can't read repo", 185 UserID: doer.ID, 186 RepoID: issue.Repo.ID, 187 } 188 } 189 } 190 191 if canDoerChangeReviewRequests { 192 return nil 193 } 194 195 return issues_model.ErrNotValidReviewRequest{ 196 Reason: "Doer can't choose reviewer", 197 UserID: doer.ID, 198 RepoID: issue.Repo.ID, 199 } 200 } 201 202 if canDoerChangeReviewRequests { 203 return nil 204 } 205 206 return issues_model.ErrNotValidReviewRequest{ 207 Reason: "Doer can't remove reviewer", 208 UserID: doer.ID, 209 RepoID: issue.Repo.ID, 210 } 211 } 212 213 // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it. 214 func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) { 215 if isAdd { 216 comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer) 217 } else { 218 comment, err = issues_model.RemoveTeamReviewRequest(ctx, issue, reviewer, doer) 219 } 220 221 if err != nil { 222 return nil, err 223 } 224 225 if comment == nil || !isAdd { 226 return nil, nil 227 } 228 229 return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment) 230 } 231 232 func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifiers []*ReviewRequestNotifier) { 233 for _, reviewNotifier := range reviewNotifiers { 234 if reviewNotifier.Reviewer != nil { 235 notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifier.Reviewer, reviewNotifier.IsAdd, reviewNotifier.Comment) 236 } else if reviewNotifier.ReviewTeam != nil { 237 if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifier.ReviewTeam, reviewNotifier.IsAdd, reviewNotifier.Comment); err != nil { 238 log.Error("teamReviewRequestNotify: %v", err) 239 } 240 } 241 } 242 } 243 244 // teamReviewRequestNotify notify all user in this team 245 func teamReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool, comment *issues_model.Comment) error { 246 // notify all user in this team 247 if err := comment.LoadIssue(ctx); err != nil { 248 return err 249 } 250 251 members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{ 252 TeamID: reviewer.ID, 253 }) 254 if err != nil { 255 return err 256 } 257 258 for _, member := range members { 259 if member.ID == comment.Issue.PosterID { 260 continue 261 } 262 comment.AssigneeID = member.ID 263 notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment) 264 } 265 266 return err 267 } 268 269 // CanDoerChangeReviewRequests returns if the doer can add/remove review requests of a PR 270 func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue) bool { 271 // The poster of the PR can change the reviewers 272 if doer.ID == issue.PosterID { 273 return true 274 } 275 276 // The owner of the repo can change the reviewers 277 if doer.ID == repo.OwnerID { 278 return true 279 } 280 281 // Collaborators of the repo can change the reviewers 282 isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, doer.ID) 283 if err != nil { 284 log.Error("IsCollaborator: %v", err) 285 return false 286 } 287 if isCollaborator { 288 return true 289 } 290 291 // If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers 292 if repo.Owner.IsOrganization() { 293 teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) 294 if err != nil { 295 log.Error("GetTeamsWithAccessToRepo: %v", err) 296 return false 297 } 298 for _, team := range teams { 299 if !team.UnitEnabled(ctx, unit.TypePullRequests) { 300 continue 301 } 302 isMember, err := organization.IsTeamMember(ctx, repo.OwnerID, team.ID, doer.ID) 303 if err != nil { 304 log.Error("IsTeamMember: %v", err) 305 continue 306 } 307 if isMember { 308 return true 309 } 310 } 311 } 312 313 return false 314 }