go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/acls/run_create.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package acls
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"go.chromium.org/luci/auth/identity"
    23  	"go.chromium.org/luci/common/errors"
    24  	"go.chromium.org/luci/common/logging"
    25  	gerritpb "go.chromium.org/luci/common/proto/gerrit"
    26  	"go.chromium.org/luci/server/auth"
    27  
    28  	"go.chromium.org/luci/cv/internal/changelist"
    29  	"go.chromium.org/luci/cv/internal/common"
    30  	"go.chromium.org/luci/cv/internal/configs/prjcfg"
    31  	"go.chromium.org/luci/cv/internal/run"
    32  
    33  	cfgpb "go.chromium.org/luci/cv/api/config/v2"
    34  )
    35  
    36  const (
    37  	okButDueToOthers                 = "CV cannot start a Run due to errors in the following CL(s)."
    38  	ownerNotCommitter                = "CV cannot start a Run for `%s` because the user is not a committer."
    39  	ownerNotDryRunner                = "CV cannot start a Run for `%s` because the user is not a dry-runner."
    40  	notOwnerNotCommitter             = "CV cannot start a Run for `%s` because the user is neither the CL owner nor a committer."
    41  	notOwnerNotCommitterNotDryRunner = "CV cannot start a Run for `%s` because the user is neither the CL owner nor a committer nor a dry-runner."
    42  
    43  	notSubmittable           = "CV cannot start a Run because this CL is not submittable. " + submitReqHint
    44  	notSubmittableWithReqs   = "CV cannot start a Run because this CL is %s. " + submitReqHint
    45  	notSubmittableSuspicious = notSubmittable + " " +
    46  		"However, all submit requirements appear to be satisfied. " +
    47  		"It's likely caused by an issue in Gerrit or Gerrit configuration. " +
    48  		"Please contact your Git admin."
    49  	submitReqHint = "Please hover over the corresponding entry in the Submit Requirements section to check what is missing."
    50  
    51  	untrustedDeps = "" +
    52  		"CV cannot start a Run because of the following dependencies. " +
    53  		"They must be submittable (please check the submit requirement) because " +
    54  		"their owners are not committers. " +
    55  		"Alternatively, you can ask the owner of this CL to trigger a dry-run."
    56  	untrustedDepsTrustDryRunnerDeps = "" +
    57  		"CV cannot start a Run because of the following dependencies. " +
    58  		"They must be submittable (please check the submit requirement) because " +
    59  		"their owners are not committers or dry-runners. " +
    60  		"Alternatively, you can ask the owner of this CL to trigger a dry-run."
    61  	untrustedDepsSuspicious = "" +
    62  		"However, some or all of the dependencies appear to satisfy all the requirements. " +
    63  		"It's likely caused by an issue in Gerrit or Gerrit configuration. " +
    64  		"Please contact your Git admin."
    65  )
    66  
    67  // runCreateChecker holds the evaluation results of a CL Run, and checks
    68  // if the Run can be created.
    69  type runCreateChecker struct {
    70  	cl                      *changelist.CL
    71  	runMode                 run.Mode
    72  	runModeDef              *cfgpb.Mode // if mode is not standard mode in CV
    73  	allowOwnerIfSubmittable cfgpb.Verifiers_GerritCQAbility_CQAction
    74  	trustDryRunnerDeps      bool
    75  	allowNonOwnerDryRunner  bool
    76  	commGroups              []string // committer groups
    77  	dryGroups               []string // dry-runner groups
    78  	newPatchsetGroups       []string // new patchset run groups
    79  
    80  	owner          identity.Identity // the CL owner
    81  	triggerer      identity.Identity // the Run triggerer
    82  	triggererEmail string            // email of the Run triggerer
    83  	submittable    bool              // if the CL is submittable in Gerrit
    84  	submitted      bool              // if the CL has been submitted in Gerrit
    85  	depsToExamine  common.CLIDs      // deps that are possibly untrusted.
    86  	trustedDeps    common.CLIDsSet   // deps that have been proven to be trustable.
    87  }
    88  
    89  func (ck runCreateChecker) canTrustDeps(ctx context.Context) (evalResult, error) {
    90  	if len(ck.depsToExamine) == 0 {
    91  		return yes, nil
    92  	}
    93  	deps := make([]*changelist.CL, 0, len(ck.depsToExamine))
    94  	for _, id := range ck.depsToExamine {
    95  		if !ck.trustedDeps.Has(id) {
    96  			deps = append(deps, &changelist.CL{ID: id})
    97  		}
    98  	}
    99  	if len(deps) == 0 {
   100  		return yes, nil
   101  	}
   102  
   103  	// Fetch the CL entity of the deps and examine if they are trustable.
   104  	// CV never removes CL entities. Hence, this handles transient and
   105  	// datastore.ErrNoSuchEntity in the same way.
   106  	if err := changelist.LoadCLs(ctx, deps); err != nil {
   107  		return no, err
   108  	}
   109  	untrusted := deps[:0]
   110  	for _, d := range deps {
   111  		// Dep is trusted, if
   112  		// - it has been submitted, OR
   113  		// - it is submittable, OR
   114  		// - the owner is a committer, OR
   115  		// - config enables trust_dry_runner_deps and the owner is a dry runner
   116  		switch submitted, err := d.Snapshot.IsSubmitted(); {
   117  		case err != nil:
   118  			return no, errors.Annotate(err, "dep-CL(%d)", d.ID).Err()
   119  		case submitted:
   120  			ck.trustedDeps.Add(d.ID)
   121  			continue
   122  		}
   123  		switch submittable, err := d.Snapshot.IsSubmittable(); {
   124  		case err != nil:
   125  			return no, errors.Annotate(err, "dep-CL(%d)", d.ID).Err()
   126  		case submittable:
   127  			ck.trustedDeps.Add(d.ID)
   128  			continue
   129  		}
   130  
   131  		depOwner, err := d.Snapshot.OwnerIdentity()
   132  		if err != nil {
   133  			return no, errors.Annotate(err, "dep-CL(%d)", d.ID).Err()
   134  		}
   135  
   136  		switch isCommitter, err := ck.isCommitter(ctx, depOwner); {
   137  		case err != nil:
   138  			return no, errors.Annotate(err,
   139  				"dep-CL(%d): checking if owner %q is a committer", d.ID, depOwner).Err()
   140  		case isCommitter:
   141  			ck.trustedDeps.Add(d.ID)
   142  			continue
   143  		}
   144  
   145  		if ck.trustDryRunnerDeps {
   146  			switch isDryRunner, err := ck.isDryRunner(ctx, depOwner); {
   147  			case err != nil:
   148  				return no, errors.Annotate(err,
   149  					"dep-CL(%d): checking if owner %q is a dry-runner", d.ID, depOwner).Err()
   150  			case isDryRunner:
   151  				ck.trustedDeps.Add(d.ID)
   152  				continue
   153  			}
   154  		}
   155  
   156  		untrusted = append(untrusted, d)
   157  	}
   158  	if len(untrusted) == 0 {
   159  		return yes, nil
   160  	}
   161  	return noWithReason(untrustedDepsReason(ctx, untrusted, ck.trustDryRunnerDeps)), nil
   162  }
   163  
   164  func (ck runCreateChecker) canCreateRun(ctx context.Context) (evalResult, error) {
   165  	switch ck.runMode {
   166  	case run.FullRun:
   167  		return ck.canCreateFullRun(ctx)
   168  	case run.DryRun:
   169  		return ck.canCreateDryRun(ctx)
   170  	case run.NewPatchsetRun:
   171  		return ck.canCreateNewPatchsetRun(ctx)
   172  	default:
   173  		// TODO(yiwzhang): Ideally, each mode should have its own ACL. Redo
   174  		// this when revamping the ACL system of LUCI CV. For now, use dry run ACL
   175  		// for mode trigger by CQ+1 and full run ACL for mode trigger by CQ+2.
   176  		switch {
   177  		case ck.runModeDef == nil:
   178  			panic(fmt.Errorf("impossible; run has non standard mode %q but mode definition is not provided", ck.runMode))
   179  		case ck.runModeDef.GetCqLabelValue() == 1:
   180  			return ck.canCreateDryRun(ctx)
   181  		case ck.runModeDef.GetCqLabelValue() == 2:
   182  			return ck.canCreateFullRun(ctx)
   183  		default:
   184  			panic(fmt.Errorf("impossible; mode specify CQ label value %d, expecting 1, or 2", ck.runModeDef.GetCqLabelValue()))
   185  		}
   186  	}
   187  }
   188  
   189  func (ck runCreateChecker) canCreateFullRun(ctx context.Context) (evalResult, error) {
   190  	// A committer can run a full run, as long as the CL is submittable.
   191  	switch isCommitter, err := ck.isCommitter(ctx, ck.triggerer); {
   192  	case err != nil:
   193  		return no, err
   194  	case isCommitter && (ck.submittable || ck.submitted):
   195  		return yes, nil
   196  	case isCommitter:
   197  		return noWithReason(notSubmittableReason(ctx, ck.cl)), nil
   198  	}
   199  	// A non-committer can trigger a full-run,
   200  	// if all of the following conditions are met.
   201  	//
   202  	// 1) triggerer == owner
   203  	// 2) triggerer is a dry-runner OR cg.AllowOwnerIfSubmittable == COMMIT
   204  	// 3) the CL is submittable in Gerrit.
   205  	//
   206  	// That is, a dry-runner can trigger a full-run for own submittable CLs
   207  	// (typically means the CL has been approved).
   208  	// For more context, crbug.com/692611 and go/cq-after-lgtm.
   209  	if ck.triggerer != ck.owner {
   210  		return noWithReason(fmt.Sprintf(notOwnerNotCommitter, ck.triggererEmail)), nil
   211  	}
   212  	isDryRunner, err := ck.isDryRunner(ctx, ck.triggerer)
   213  	if err != nil {
   214  		return no, err
   215  	}
   216  	if !isDryRunner && ck.allowOwnerIfSubmittable != cfgpb.Verifiers_GerritCQAbility_COMMIT {
   217  		return noWithReason(fmt.Sprintf(ownerNotCommitter, ck.triggererEmail)), nil
   218  	}
   219  	if !ck.submittable && !ck.submitted {
   220  		return noWithReason(notSubmittableReason(ctx, ck.cl)), nil
   221  	}
   222  	return yes, nil
   223  }
   224  
   225  func (ck runCreateChecker) canCreateNewPatchsetRun(ctx context.Context) (evalResult, error) {
   226  	switch isNPRunner, err := ck.isNewPatchsetRunner(ctx, ck.owner); {
   227  	case err != nil:
   228  		return no, err
   229  	case isNPRunner:
   230  		return yes, nil
   231  	default:
   232  		return noWithReason("CL owner is not in the allowlist."), nil
   233  	}
   234  }
   235  
   236  func (ck runCreateChecker) canCreateDryRun(ctx context.Context) (evalResult, error) {
   237  	isCommitter, err := ck.isCommitter(ctx, ck.triggerer)
   238  	if err != nil {
   239  		return no, err
   240  	}
   241  	if isCommitter {
   242  		switch {
   243  		case ck.triggerer == ck.owner:
   244  			// A committer can trigger a dry run on their own CL
   245  			// without CL being submittable. We assume dependencies
   246  			// are trusted since they uploaded the CL.
   247  			return yes, nil
   248  		default:
   249  			// In order for a committer to trigger a dry-run for someone
   250  			// else's CL, all the dependencies must be trusted
   251  			// dependencies.
   252  			return ck.canTrustDeps(ctx)
   253  		}
   254  	}
   255  
   256  	// A non-committer can trigger a dry-run if they are a dry-runner.
   257  	isDryRunner, err := ck.isDryRunner(ctx, ck.triggerer)
   258  	if err != nil {
   259  		return no, err
   260  	}
   261  	if isDryRunner {
   262  		switch {
   263  		case ck.triggerer == ck.owner:
   264  			// A dry-runner can trigger a dry run on their own CL without CL being
   265  			// submittable. We assume dependencies are trusted since they uploaded
   266  			// the CL.
   267  			return yes, nil
   268  		case ck.allowNonOwnerDryRunner:
   269  			// A dry-runner can trigger a dry run on a CL they don't own if
   270  			// allowNonOwnerDryRunner is set. All dependencies must be trusted.
   271  			return ck.canTrustDeps(ctx)
   272  		default:
   273  			// Otherwise, a dry-runner cannot trigger a dry run on
   274  			// a CL they don't own.
   275  			return noWithReason(fmt.Sprintf(notOwnerNotCommitter, ck.triggererEmail)), nil
   276  		}
   277  	}
   278  
   279  	// One can trigger a dry-run without being a dry-runner or a committer,
   280  	// if all the following conditions are met:
   281  	//
   282  	// 1) triggerer == owner
   283  	// 2) cg.AllowOwnerIfSubmittable in [COMMIT, DRY_RUN]
   284  	// 3) The CL is submittable in Gerrit.
   285  	// 4) All the deps are trusted.
   286  	//
   287  	// A dep is trusted, if at least one of the following conditions are met.
   288  	// - the dep is one of the CLs included in the Run
   289  	// - the owner of the dep is a committer
   290  	// - the dep is submittable in Gerrit
   291  	//
   292  	// For more context, crbug.com/692611 and go/cq-after-lgtm.
   293  	if ck.triggerer != ck.owner {
   294  		reason := notOwnerNotCommitter
   295  		if ck.allowNonOwnerDryRunner {
   296  			reason = notOwnerNotCommitterNotDryRunner
   297  		}
   298  		return noWithReason(fmt.Sprintf(reason, ck.triggererEmail)), nil
   299  	}
   300  
   301  	switch ck.allowOwnerIfSubmittable {
   302  	case cfgpb.Verifiers_GerritCQAbility_DRY_RUN:
   303  	case cfgpb.Verifiers_GerritCQAbility_COMMIT:
   304  	default:
   305  		return noWithReason(fmt.Sprintf(ownerNotDryRunner, ck.triggererEmail)), nil
   306  	}
   307  	if !ck.submittable && !ck.submitted {
   308  		return noWithReason(notSubmittableReason(ctx, ck.cl)), nil
   309  	}
   310  	return ck.canTrustDeps(ctx)
   311  }
   312  
   313  func (ck runCreateChecker) isDryRunner(ctx context.Context, id identity.Identity) (bool, error) {
   314  	if len(ck.dryGroups) == 0 {
   315  		return false, nil
   316  	}
   317  	return auth.GetState(ctx).DB().IsMember(ctx, id, ck.dryGroups)
   318  }
   319  
   320  func (ck runCreateChecker) isNewPatchsetRunner(ctx context.Context, id identity.Identity) (bool, error) {
   321  	if len(ck.newPatchsetGroups) == 0 {
   322  		return false, nil
   323  	}
   324  	return auth.GetState(ctx).DB().IsMember(ctx, id, ck.newPatchsetGroups)
   325  }
   326  
   327  func (ck runCreateChecker) isCommitter(ctx context.Context, id identity.Identity) (bool, error) {
   328  	if len(ck.commGroups) == 0 {
   329  		return false, nil
   330  	}
   331  	return auth.GetState(ctx).DB().IsMember(ctx, id, ck.commGroups)
   332  }
   333  
   334  // CheckRunCreate verifies that the user(s) who triggered Run are authorized
   335  // to create the Run for the CLs.
   336  func CheckRunCreate(ctx context.Context, cg *prjcfg.ConfigGroup, trs []*run.Trigger, cls []*changelist.CL) (CheckResult, error) {
   337  	res := make(CheckResult, len(cls))
   338  	cks, err := evaluateCLs(ctx, cg, trs, cls)
   339  	if err != nil {
   340  		return nil, err
   341  	}
   342  	for _, ck := range cks {
   343  		switch result, err := ck.canCreateRun(ctx); {
   344  		case err != nil:
   345  			return nil, err
   346  		case !result.ok:
   347  			res[ck.cl] = result.reason
   348  		}
   349  	}
   350  	return res, nil
   351  }
   352  
   353  func evaluateCLs(ctx context.Context, cg *prjcfg.ConfigGroup, trs []*run.Trigger, cls []*changelist.CL) ([]*runCreateChecker, error) {
   354  	gVerifier := cg.Content.Verifiers.GetGerritCqAbility()
   355  
   356  	cks := make([]*runCreateChecker, len(cls))
   357  	trustedDeps := make(common.CLIDsSet, len(cls))
   358  	for i, cl := range cls {
   359  		tr := trs[i]
   360  		triggerer, err := identity.MakeIdentity(fmt.Sprintf("%s:%s", identity.User, tr.Email))
   361  		if err != nil {
   362  			return nil, errors.Annotate(err, "CL(%d): triggerer %q", cl.ID, tr.Email).Err()
   363  		}
   364  		owner, err := cl.Snapshot.OwnerIdentity()
   365  		if err != nil {
   366  			return nil, errors.Annotate(err, "CL(%d)", cl.ID).Err()
   367  		}
   368  
   369  		submittable, err := cl.Snapshot.IsSubmittable()
   370  		if err != nil {
   371  			return nil, errors.Annotate(err, "CL(%d)", cl.ID).Err()
   372  		}
   373  		submitted, err := cl.Snapshot.IsSubmitted()
   374  		if err != nil {
   375  			return nil, errors.Annotate(err, "CL(%d)", cl.ID).Err()
   376  		}
   377  		// by default, all deps are untrusted, unless they are part of the Run.
   378  		var depsToExamine common.CLIDs
   379  		if len(cl.Snapshot.Deps) > 0 {
   380  			depsToExamine = make(common.CLIDs, len(cl.Snapshot.Deps))
   381  			for i, d := range cl.Snapshot.Deps {
   382  				depsToExamine[i] = common.CLID(d.Clid)
   383  			}
   384  		}
   385  		trustedDeps.Add(cl.ID)
   386  		cks[i] = &runCreateChecker{
   387  			cl:                      cl,
   388  			runMode:                 run.Mode(tr.Mode),
   389  			runModeDef:              tr.GetModeDefinition(),
   390  			allowOwnerIfSubmittable: gVerifier.GetAllowOwnerIfSubmittable(),
   391  			trustDryRunnerDeps:      gVerifier.GetTrustDryRunnerDeps(),
   392  			allowNonOwnerDryRunner:  gVerifier.GetAllowNonOwnerDryRunner(),
   393  			commGroups:              gVerifier.GetCommitterList(),
   394  			dryGroups:               gVerifier.GetDryRunAccessList(),
   395  			newPatchsetGroups:       gVerifier.GetNewPatchsetRunAccessList(),
   396  
   397  			owner:          owner,
   398  			triggerer:      triggerer,
   399  			triggererEmail: tr.Email,
   400  			submittable:    submittable,
   401  			submitted:      submitted,
   402  			depsToExamine:  depsToExamine,
   403  			trustedDeps:    trustedDeps,
   404  		}
   405  	}
   406  	return cks, nil
   407  }
   408  
   409  // untrustedDepsReason generates a RunCreate rejection comment for untrusted deps.
   410  func untrustedDepsReason(ctx context.Context, udeps []*changelist.CL, trustDryRunnerDeps bool) string {
   411  	var sb strings.Builder
   412  	anySuspicious := false
   413  	if trustDryRunnerDeps {
   414  		sb.WriteString(untrustedDepsTrustDryRunnerDeps)
   415  	} else {
   416  		sb.WriteString(untrustedDeps)
   417  	}
   418  	for _, d := range udeps {
   419  		fmt.Fprintf(&sb, "\n- %s:", d.ExternalID.MustURL())
   420  		if allSatisfied, msg := strSubmitReqsForNotSubmittableCL(ctx, d); len(msg) > 0 {
   421  			fmt.Fprintf(&sb, " %s", msg)
   422  			anySuspicious = anySuspicious || allSatisfied
   423  		}
   424  	}
   425  	if anySuspicious {
   426  		fmt.Fprintf(&sb, "\n\n%s", untrustedDepsSuspicious)
   427  	}
   428  	return sb.String()
   429  }
   430  
   431  // notSubmittableReason generates a RunCreate rejection comment for not
   432  // submittable CL.
   433  func notSubmittableReason(ctx context.Context, cl *changelist.CL) string {
   434  	switch allSatisfied, msg := strSubmitReqsForNotSubmittableCL(ctx, cl); {
   435  	case allSatisfied:
   436  		return notSubmittableSuspicious
   437  	case len(msg) > 0:
   438  		return fmt.Sprintf(notSubmittableWithReqs, msg)
   439  	}
   440  	return notSubmittable
   441  }
   442  
   443  func strSubmitReqsForNotSubmittableCL(ctx context.Context, cl *changelist.CL) (allSatisfied bool, msg string) {
   444  	reqs := cl.Snapshot.GetGerrit().GetInfo().GetSubmitRequirements()
   445  	if len(reqs) == 0 {
   446  		return
   447  	}
   448  	join := func(ss []string) string {
   449  		switch len(ss) {
   450  		case 0:
   451  			return ""
   452  		case 1:
   453  			return fmt.Sprintf("`%s`", ss[0])
   454  		case 2:
   455  			return fmt.Sprintf("`%s` and `%s`", ss[0], ss[1])
   456  		default:
   457  			last := len(ss) - 1
   458  			return fmt.Sprintf("`%s`, and `%s`", strings.Join(ss[:last], "`, `"), ss[last])
   459  		}
   460  	}
   461  
   462  	switch satisfied, unsatisfied := groupSubmitReqs(ctx, reqs); {
   463  	case len(unsatisfied) == 0:
   464  		switch len(satisfied) {
   465  		case 0:
   466  			// all were NOT_APPLICABLE?
   467  			// just log the occurrence, but consider that
   468  			// submit requirements agreed with Submittable.
   469  			logging.Errorf(ctx, "CL(%d): all submit reqs(%d) are NOT_APPLICABLE", cl.ID, len(reqs))
   470  		case 1:
   471  			msg = fmt.Sprintf("not submittable, although submit requirement `%s` is satisfied", satisfied[0])
   472  		default:
   473  			msg = fmt.Sprintf("not submittable, although submit requirements %s are satisfied", join(satisfied))
   474  		}
   475  		allSatisfied = len(satisfied) != 0
   476  	default:
   477  		msg = fmt.Sprintf("not satisfying the %s submit requirement", join(unsatisfied))
   478  	}
   479  	if allSatisfied {
   480  		logging.Errorf(ctx, "CL(%d): all submit reqs satisfied; but CL not submittable", cl.ID)
   481  	}
   482  	return
   483  }
   484  
   485  func groupSubmitReqs(ctx context.Context, reqs []*gerritpb.SubmitRequirementResultInfo) (satisfied, unsatisfied []string) {
   486  	if len(reqs) == 0 {
   487  		return
   488  	}
   489  	satisfied = make([]string, 0, len(reqs))
   490  	unsatisfied = make([]string, 0, len(reqs))
   491  	for _, req := range reqs {
   492  		switch req.Status {
   493  		case gerritpb.SubmitRequirementResultInfo_SUBMIT_REQUIREMENT_STATUS_UNSPECIFIED:
   494  			panic(errors.New("Unspecified SubmitRequirement.Status; this should never happen"))
   495  		case gerritpb.SubmitRequirementResultInfo_NOT_APPLICABLE:
   496  
   497  		// satisfied statuses
   498  		case gerritpb.SubmitRequirementResultInfo_SATISFIED,
   499  			gerritpb.SubmitRequirementResultInfo_OVERRIDDEN,
   500  			gerritpb.SubmitRequirementResultInfo_FORCED:
   501  			satisfied = append(satisfied, req.Name)
   502  
   503  		// unsatisfied statuses
   504  		case gerritpb.SubmitRequirementResultInfo_ERROR:
   505  			// log the error. It may be helpful for diagnosing the reason of a Run rejection.
   506  			logging.Warningf(ctx, "Gerrit reported SubmissionRequirement error %s", req)
   507  			fallthrough
   508  		case gerritpb.SubmitRequirementResultInfo_UNSATISFIED:
   509  			unsatisfied = append(unsatisfied, req.Name)
   510  
   511  		default:
   512  			// This must be a bug in CV.
   513  			//
   514  			// common/api/gerrit returns an error if it receives a Status of which enum
   515  			// doesn't exist in common/proto/gerrit. Hence, if a Status is unknown here,
   516  			// this switch is missing the status, enumerated in common/proto/gerrit.
   517  			logging.Errorf(ctx, "Unknown SubmitRequirementStatus %q", req.GetStatus())
   518  			// Unknown enums are considered as a not-satisfied status.
   519  			unsatisfied = append(unsatisfied, req.Name)
   520  		}
   521  	}
   522  	return
   523  }