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  }