code.gitea.io/gitea@v1.21.7/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  	"code.gitea.io/gitea/models/unit"
    14  	user_model "code.gitea.io/gitea/models/user"
    15  	"code.gitea.io/gitea/modules/log"
    16  	notify_service "code.gitea.io/gitea/services/notify"
    17  )
    18  
    19  // DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
    20  func DeleteNotPassedAssignee(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assignees []*user_model.User) (err error) {
    21  	var found bool
    22  	oriAssignes := make([]*user_model.User, len(issue.Assignees))
    23  	_ = copy(oriAssignes, issue.Assignees)
    24  
    25  	for _, assignee := range oriAssignes {
    26  		found = false
    27  		for _, alreadyAssignee := range assignees {
    28  			if assignee.ID == alreadyAssignee.ID {
    29  				found = true
    30  				break
    31  			}
    32  		}
    33  
    34  		if !found {
    35  			// This function also does comments and hooks, which is why we call it separately instead of directly removing the assignees here
    36  			if _, _, err := ToggleAssigneeWithNotify(ctx, issue, doer, assignee.ID); err != nil {
    37  				return err
    38  			}
    39  		}
    40  	}
    41  
    42  	return nil
    43  }
    44  
    45  // ToggleAssigneeWithNoNotify changes a user between assigned and not assigned for this issue, and make issue comment for it.
    46  func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *issues_model.Comment, err error) {
    47  	removed, comment, err = issues_model.ToggleIssueAssignee(ctx, issue, doer, assigneeID)
    48  	if err != nil {
    49  		return false, nil, err
    50  	}
    51  
    52  	assignee, err := user_model.GetUserByID(ctx, assigneeID)
    53  	if err != nil {
    54  		return false, nil, err
    55  	}
    56  
    57  	notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, removed, comment)
    58  
    59  	return removed, comment, err
    60  }
    61  
    62  // ReviewRequest add or remove a review request from a user for this PR, and make comment for it.
    63  func ReviewRequest(ctx context.Context, issue *issues_model.Issue, doer, reviewer *user_model.User, isAdd bool) (comment *issues_model.Comment, err error) {
    64  	if isAdd {
    65  		comment, err = issues_model.AddReviewRequest(ctx, issue, reviewer, doer)
    66  	} else {
    67  		comment, err = issues_model.RemoveReviewRequest(ctx, issue, reviewer, doer)
    68  	}
    69  
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	if comment != nil {
    75  		notify_service.PullRequestReviewRequest(ctx, doer, issue, reviewer, isAdd, comment)
    76  	}
    77  
    78  	return comment, err
    79  }
    80  
    81  // IsValidReviewRequest Check permission for ReviewRequest
    82  func IsValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User, isAdd bool, issue *issues_model.Issue, permDoer *access_model.Permission) error {
    83  	if reviewer.IsOrganization() {
    84  		return issues_model.ErrNotValidReviewRequest{
    85  			Reason: "Organization can't be added as reviewer",
    86  			UserID: doer.ID,
    87  			RepoID: issue.Repo.ID,
    88  		}
    89  	}
    90  	if doer.IsOrganization() {
    91  		return issues_model.ErrNotValidReviewRequest{
    92  			Reason: "Organization can't be doer to add reviewer",
    93  			UserID: doer.ID,
    94  			RepoID: issue.Repo.ID,
    95  		}
    96  	}
    97  
    98  	permReviewer, err := access_model.GetUserRepoPermission(ctx, issue.Repo, reviewer)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	if permDoer == nil {
   104  		permDoer = new(access_model.Permission)
   105  		*permDoer, err = access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
   106  		if err != nil {
   107  			return err
   108  		}
   109  	}
   110  
   111  	lastreview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
   112  	if err != nil && !issues_model.IsErrReviewNotExist(err) {
   113  		return err
   114  	}
   115  
   116  	var pemResult bool
   117  	if isAdd {
   118  		pemResult = permReviewer.CanAccessAny(perm.AccessModeRead, unit.TypePullRequests)
   119  		if !pemResult {
   120  			return issues_model.ErrNotValidReviewRequest{
   121  				Reason: "Reviewer can't read",
   122  				UserID: doer.ID,
   123  				RepoID: issue.Repo.ID,
   124  			}
   125  		}
   126  
   127  		if doer.ID == issue.PosterID && issue.OriginalAuthorID == 0 && lastreview != nil && lastreview.Type != issues_model.ReviewTypeRequest {
   128  			return nil
   129  		}
   130  
   131  		pemResult = doer.ID == issue.PosterID
   132  		if !pemResult {
   133  			pemResult = permDoer.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
   134  		}
   135  		if !pemResult {
   136  			pemResult, err = issues_model.IsOfficialReviewer(ctx, issue, doer)
   137  			if err != nil {
   138  				return err
   139  			}
   140  			if !pemResult {
   141  				return issues_model.ErrNotValidReviewRequest{
   142  					Reason: "Doer can't choose reviewer",
   143  					UserID: doer.ID,
   144  					RepoID: issue.Repo.ID,
   145  				}
   146  			}
   147  		}
   148  
   149  		if reviewer.ID == issue.PosterID && issue.OriginalAuthorID == 0 {
   150  			return issues_model.ErrNotValidReviewRequest{
   151  				Reason: "poster of pr can't be reviewer",
   152  				UserID: doer.ID,
   153  				RepoID: issue.Repo.ID,
   154  			}
   155  		}
   156  	} else {
   157  		if lastreview != nil && lastreview.Type == issues_model.ReviewTypeRequest && lastreview.ReviewerID == doer.ID {
   158  			return nil
   159  		}
   160  
   161  		pemResult = permDoer.IsAdmin()
   162  		if !pemResult {
   163  			return issues_model.ErrNotValidReviewRequest{
   164  				Reason: "Doer is not admin",
   165  				UserID: doer.ID,
   166  				RepoID: issue.Repo.ID,
   167  			}
   168  		}
   169  	}
   170  
   171  	return nil
   172  }
   173  
   174  // IsValidTeamReviewRequest Check permission for ReviewRequest Team
   175  func IsValidTeamReviewRequest(ctx context.Context, reviewer *organization.Team, doer *user_model.User, isAdd bool, issue *issues_model.Issue) error {
   176  	if doer.IsOrganization() {
   177  		return issues_model.ErrNotValidReviewRequest{
   178  			Reason: "Organization can't be doer to add reviewer",
   179  			UserID: doer.ID,
   180  			RepoID: issue.Repo.ID,
   181  		}
   182  	}
   183  
   184  	permission, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
   185  	if err != nil {
   186  		log.Error("Unable to GetUserRepoPermission for %-v in %-v#%d", doer, issue.Repo, issue.Index)
   187  		return err
   188  	}
   189  
   190  	if isAdd {
   191  		if issue.Repo.IsPrivate {
   192  			hasTeam := organization.HasTeamRepo(ctx, reviewer.OrgID, reviewer.ID, issue.RepoID)
   193  
   194  			if !hasTeam {
   195  				return issues_model.ErrNotValidReviewRequest{
   196  					Reason: "Reviewing team can't read repo",
   197  					UserID: doer.ID,
   198  					RepoID: issue.Repo.ID,
   199  				}
   200  			}
   201  		}
   202  
   203  		doerCanWrite := permission.CanAccessAny(perm.AccessModeWrite, unit.TypePullRequests)
   204  		if !doerCanWrite && doer.ID != issue.PosterID {
   205  			official, err := issues_model.IsOfficialReviewer(ctx, issue, doer)
   206  			if err != nil {
   207  				log.Error("Unable to Check if IsOfficialReviewer for %-v in %-v#%d", doer, issue.Repo, issue.Index)
   208  				return err
   209  			}
   210  			if !official {
   211  				return issues_model.ErrNotValidReviewRequest{
   212  					Reason: "Doer can't choose reviewer",
   213  					UserID: doer.ID,
   214  					RepoID: issue.Repo.ID,
   215  				}
   216  			}
   217  		}
   218  	} else if !permission.IsAdmin() {
   219  		return issues_model.ErrNotValidReviewRequest{
   220  			Reason: "Only admin users can remove team requests. Doer is not admin",
   221  			UserID: doer.ID,
   222  			RepoID: issue.Repo.ID,
   223  		}
   224  	}
   225  
   226  	return nil
   227  }
   228  
   229  // TeamReviewRequest add or remove a review request from a team for this PR, and make comment for it.
   230  func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewer *organization.Team, isAdd bool) (comment *issues_model.Comment, err error) {
   231  	if isAdd {
   232  		comment, err = issues_model.AddTeamReviewRequest(ctx, issue, reviewer, doer)
   233  	} else {
   234  		comment, err = issues_model.RemoveTeamReviewRequest(ctx, issue, reviewer, doer)
   235  	}
   236  
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  
   241  	if comment == nil || !isAdd {
   242  		return nil, nil
   243  	}
   244  
   245  	// notify all user in this team
   246  	if err := comment.LoadIssue(ctx); err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	members, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
   251  		TeamID: reviewer.ID,
   252  	})
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  
   257  	for _, member := range members {
   258  		if member.ID == comment.Issue.PosterID {
   259  			continue
   260  		}
   261  		comment.AssigneeID = member.ID
   262  		notify_service.PullRequestReviewRequest(ctx, doer, issue, member, isAdd, comment)
   263  	}
   264  
   265  	return comment, err
   266  }