go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/acls/run_create_test.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 "fmt" 19 "testing" 20 21 "go.chromium.org/luci/auth/identity" 22 "go.chromium.org/luci/gae/service/datastore" 23 "go.chromium.org/luci/server/auth" 24 "go.chromium.org/luci/server/auth/authtest" 25 26 gerritpb "go.chromium.org/luci/common/proto/gerrit" 27 28 cfgpb "go.chromium.org/luci/cv/api/config/v2" 29 "go.chromium.org/luci/cv/internal/changelist" 30 "go.chromium.org/luci/cv/internal/common" 31 "go.chromium.org/luci/cv/internal/configs/prjcfg" 32 "go.chromium.org/luci/cv/internal/cvtesting" 33 "go.chromium.org/luci/cv/internal/run" 34 35 . "github.com/smartystreets/goconvey/convey" 36 ) 37 38 func TestCheckRunCLs(t *testing.T) { 39 t.Parallel() 40 41 const ( 42 lProject = "chromium" 43 gerritHost = "chromium-review.googlesource.com" 44 committers = "committer-group" 45 dryRunners = "dry-runner-group" 46 npRunners = "new-patchset-runner-group" 47 ) 48 49 Convey("CheckRunCreate", t, func() { 50 ct := cvtesting.Test{} 51 ctx, cancel := ct.SetUp(t) 52 defer cancel() 53 cg := prjcfg.ConfigGroup{ 54 Content: &cfgpb.ConfigGroup{ 55 Verifiers: &cfgpb.Verifiers{ 56 GerritCqAbility: &cfgpb.Verifiers_GerritCQAbility{ 57 CommitterList: []string{committers}, 58 DryRunAccessList: []string{dryRunners}, 59 NewPatchsetRunAccessList: []string{npRunners}, 60 }, 61 }, 62 }, 63 } 64 65 authState := &authtest.FakeState{FakeDB: authtest.NewFakeDB()} 66 ctx = auth.WithState(ctx, authState) 67 addMember := func(email, grp string) { 68 id, err := identity.MakeIdentity(fmt.Sprintf("%s:%s", identity.User, email)) 69 So(err, ShouldBeNil) 70 authState.FakeDB.(*authtest.FakeDB).AddMocks(authtest.MockMembership(id, grp)) 71 } 72 addCommitter := func(email string) { 73 addMember(email, committers) 74 } 75 addDryRunner := func(email string) { 76 addMember(email, dryRunners) 77 } 78 addNPRunner := func(email string) { 79 addMember(email, npRunners) 80 } 81 82 // test helpers 83 var cls []*changelist.CL 84 var trs []*run.Trigger 85 var clid int64 86 addCL := func(triggerer, owner string, m run.Mode) (*changelist.CL, *run.Trigger) { 87 clid++ 88 cl := &changelist.CL{ 89 ID: common.CLID(clid), 90 ExternalID: changelist.MustGobID(gerritHost, clid), 91 Snapshot: &changelist.Snapshot{ 92 Kind: &changelist.Snapshot_Gerrit{ 93 Gerrit: &changelist.Gerrit{ 94 Host: gerritHost, 95 Info: &gerritpb.ChangeInfo{ 96 Owner: &gerritpb.AccountInfo{ 97 Email: owner, 98 }, 99 }, 100 }, 101 }, 102 }, 103 } 104 So(datastore.Put(ctx, cl), ShouldBeNil) 105 cls = append(cls, cl) 106 tr := &run.Trigger{ 107 Email: triggerer, 108 Mode: string(m), 109 } 110 trs = append(trs, tr) 111 return cl, tr 112 } 113 addDep := func(base *changelist.CL, owner string) *changelist.CL { 114 clid++ 115 dep := &changelist.CL{ 116 ID: common.CLID(clid), 117 ExternalID: changelist.MustGobID(gerritHost, clid), 118 Snapshot: &changelist.Snapshot{ 119 Kind: &changelist.Snapshot_Gerrit{ 120 Gerrit: &changelist.Gerrit{ 121 Host: gerritHost, 122 Info: &gerritpb.ChangeInfo{ 123 Owner: &gerritpb.AccountInfo{ 124 Email: owner, 125 }, 126 }, 127 }, 128 }, 129 }, 130 } 131 So(datastore.Put(ctx, dep), ShouldBeNil) 132 base.Snapshot.Deps = append(base.Snapshot.Deps, &changelist.Dep{Clid: clid}) 133 return dep 134 } 135 136 mustOK := func() { 137 res, err := CheckRunCreate(ctx, &cg, trs, cls) 138 So(err, ShouldBeNil) 139 So(res.FailuresSummary(), ShouldBeEmpty) 140 So(res.OK(), ShouldBeTrue) 141 } 142 mustFailWith := func(cl *changelist.CL, format string, args ...any) CheckResult { 143 res, err := CheckRunCreate(ctx, &cg, trs, cls) 144 So(err, ShouldBeNil) 145 So(res.OK(), ShouldBeFalse) 146 So(res.Failure(cl), ShouldContainSubstring, fmt.Sprintf(format, args...)) 147 return res 148 } 149 markCLSubmittable := func(cl *changelist.CL) { 150 cl.Snapshot.GetGerrit().GetInfo().Submittable = true 151 So(datastore.Put(ctx, cl), ShouldBeNil) 152 } 153 submitCL := func(cl *changelist.CL) { 154 cl.Snapshot.GetGerrit().GetInfo().Status = gerritpb.ChangeStatus_MERGED 155 So(datastore.Put(ctx, cl), ShouldBeNil) 156 } 157 setAllowOwner := func(action cfgpb.Verifiers_GerritCQAbility_CQAction) { 158 cg.Content.Verifiers.GerritCqAbility.AllowOwnerIfSubmittable = action 159 } 160 setTrustDryRunnerDeps := func(trust bool) { 161 cg.Content.Verifiers.GerritCqAbility.TrustDryRunnerDeps = trust 162 } 163 setAllowNonOwnerDryRunner := func(allow bool) { 164 cg.Content.Verifiers.GerritCqAbility.AllowNonOwnerDryRunner = allow 165 } 166 167 addSubmitReq := func(cl *changelist.CL, name string, st gerritpb.SubmitRequirementResultInfo_Status) { 168 ci := cl.Snapshot.Kind.(*changelist.Snapshot_Gerrit).Gerrit.Info 169 ci.SubmitRequirements = append(ci.SubmitRequirements, 170 &gerritpb.SubmitRequirementResultInfo{Name: name, Status: st}) 171 So(datastore.Put(ctx, cl), ShouldBeNil) 172 } 173 satisfiedReq := func(cl *changelist.CL, name string) { 174 addSubmitReq(cl, name, gerritpb.SubmitRequirementResultInfo_SATISFIED) 175 } 176 unsatisfiedReq := func(cl *changelist.CL, name string) { 177 addSubmitReq(cl, name, gerritpb.SubmitRequirementResultInfo_UNSATISFIED) 178 } 179 naReq := func(cl *changelist.CL, name string) { 180 addSubmitReq(cl, name, gerritpb.SubmitRequirementResultInfo_NOT_APPLICABLE) 181 } 182 183 Convey("mode == FullRun", func() { 184 m := run.FullRun 185 186 Convey("triggerer == owner", func() { 187 tr, owner := "t@example.org", "t@example.org" 188 cl, _ := addCL(tr, owner, m) 189 190 Convey("triggerer is a committer", func() { 191 addCommitter(tr) 192 193 // Should succeed when CL becomes submittable. 194 mustFailWith(cl, notSubmittable) 195 markCLSubmittable(cl) 196 mustOK() 197 }) 198 Convey("triggerer is a dry-runner", func() { 199 addDryRunner(tr) 200 201 // Dry-runner can trigger a full-run for own submittable CL. 202 unsatisfiedReq(cl, "Code-Review") 203 204 mustFailWith(cl, "CV cannot start a Run because this CL is not satisfying the `Code-Review` submit requirement. Please hover over the corresponding entry in the Submit Requirements section to check what is missing.") 205 markCLSubmittable(cl) 206 mustOK() 207 }) 208 Convey("triggerer is neither dry-runner nor committer", func() { 209 Convey("CL submittable", func() { 210 // Should fail, even if it was submittable. 211 markCLSubmittable(cl) 212 mustFailWith(cl, "CV cannot start a Run for `%s` because the user is not a committer", tr) 213 // unless AllowOwnerIfSubmittable == COMMIT 214 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_COMMIT) 215 mustOK() 216 }) 217 Convey("CL not submittable", func() { 218 // Should fail always. 219 mustFailWith(cl, "CV cannot start a Run for `%s` because the user is not a committer", tr) 220 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_COMMIT) 221 mustFailWith(cl, notSubmittable) 222 }) 223 }) 224 Convey("suspiciously not submittable", func() { 225 addDryRunner(tr) 226 addSubmitReq(cl, "Code-Review", gerritpb.SubmitRequirementResultInfo_SATISFIED) 227 mustFailWith(cl, notSubmittableSuspicious) 228 }) 229 }) 230 231 Convey("triggerer != owner", func() { 232 tr, owner := "t@example.org", "o@example.org" 233 cl, _ := addCL(tr, owner, m) 234 235 Convey("triggerer is a committer", func() { 236 addCommitter(tr) 237 238 // Should succeed when CL becomes submittable. 239 mustFailWith(cl, notSubmittable) 240 markCLSubmittable(cl) 241 mustOK() 242 }) 243 Convey("triggerer is a dry-runner", func() { 244 addDryRunner(tr) 245 246 // Dry-runner cannot trigger a full-run for someone else' CL, 247 // whether it is submittable or not 248 mustFailWith(cl, "neither the CL owner nor a committer") 249 markCLSubmittable(cl) 250 mustFailWith(cl, "neither the CL owner nor a committer") 251 252 // AllowOwnerIfSubmittable doesn't change the decision, either. 253 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_COMMIT) 254 mustFailWith(cl, "neither the CL owner nor a committer") 255 256 // TrustDryRunnerDeps doesn't change the decision, either. 257 setTrustDryRunnerDeps(true) 258 mustFailWith(cl, "neither the CL owner nor a committer") 259 260 // AllowNonOwnerDryRunner doesn't change the decision, either. 261 setAllowNonOwnerDryRunner(true) 262 mustFailWith(cl, "neither the CL owner nor a committer") 263 }) 264 Convey("triggerer is neither dry-runner nor committer", func() { 265 // Should fail always. 266 mustFailWith(cl, "neither the CL owner nor a committer") 267 markCLSubmittable(cl) 268 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_COMMIT) 269 mustFailWith(cl, "neither the CL owner nor a committer") 270 }) 271 Convey("suspiciously not submittable", func() { 272 addCommitter(tr) 273 addSubmitReq(cl, "Code-Review", gerritpb.SubmitRequirementResultInfo_SATISFIED) 274 mustFailWith(cl, notSubmittableSuspicious) 275 }) 276 }) 277 }) 278 279 Convey("mode == DryRun", func() { 280 m := run.DryRun 281 282 Convey("triggerer == owner", func() { 283 tr, owner := "t@example.org", "t@example.org" 284 cl, _ := addCL(tr, owner, m) 285 286 Convey("triggerer is a committer", func() { 287 // Committers can trigger a dry-run for someone else' CL 288 // even if the CL is not submittable 289 addCommitter(tr) 290 mustOK() 291 }) 292 Convey("triggerer is a dry-runner", func() { 293 // Should succeed even if the CL is not submittable. 294 addDryRunner(tr) 295 mustOK() 296 }) 297 Convey("triggerer is neither dry-runner nor committer", func() { 298 Convey("CL submittable", func() { 299 // Should fail, even if the CL is submittable. 300 markCLSubmittable(cl) 301 mustFailWith(cl, "CV cannot start a Run for `%s` because the user is not a dry-runner", owner) 302 // Unless AllowOwnerIfSubmittable == DRY_RUN 303 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_DRY_RUN) 304 mustOK() 305 // Or, COMMIT 306 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_COMMIT) 307 mustOK() 308 }) 309 Convey("CL not submittable", func() { 310 // Should fail always. 311 mustFailWith(cl, "CV cannot start a Run for `%s` because the user is not a dry-runner", owner) 312 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_COMMIT) 313 mustFailWith(cl, notSubmittable) 314 }) 315 }) 316 }) 317 318 Convey("triggerer != owner", func() { 319 tr, owner := "t@example.org", "o@example.org" 320 cl, _ := addCL(tr, owner, m) 321 322 Convey("triggerer is a committer", func() { 323 // Should succeed whether CL is submittable or not. 324 addCommitter(tr) 325 mustOK() 326 markCLSubmittable(cl) 327 mustOK() 328 }) 329 Convey("triggerer is a dry-runner", func() { 330 // Only committers can trigger a dry-run for someone else's CL. 331 addDryRunner(tr) 332 mustFailWith(cl, "neither the CL owner nor a committer") 333 markCLSubmittable(cl) 334 mustFailWith(cl, "neither the CL owner nor a committer") 335 // AllowOwnerIfSubmittable doesn't change the decision, either. 336 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_COMMIT) 337 mustFailWith(cl, "neither the CL owner nor a committer") 338 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_DRY_RUN) 339 mustFailWith(cl, "neither the CL owner nor a committer") 340 // TrustDryRunnerDeps doesn't change the decision, either. 341 setTrustDryRunnerDeps(true) 342 mustFailWith(cl, "neither the CL owner nor a committer") 343 }) 344 Convey("triggerer is a dry-runner (with allow_non_owner_dry_runner)", func() { 345 // With allow_non_owner_dry_runner, dry-runners can trigger a dry-run for someone else's CL. 346 addDryRunner(tr) 347 setAllowNonOwnerDryRunner(true) 348 mustOK() 349 markCLSubmittable(cl) 350 mustOK() 351 }) 352 Convey("triggerer is neither dry-runner nor committer", func() { 353 // Only committers can trigger a dry-run for someone else' CL. 354 mustFailWith(cl, "neither the CL owner nor a committer") 355 markCLSubmittable(cl) 356 mustFailWith(cl, "neither the CL owner nor a committer") 357 // AllowOwnerIfSubmittable doesn't change the decision, either. 358 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_COMMIT) 359 mustFailWith(cl, "neither the CL owner nor a committer") 360 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_DRY_RUN) 361 mustFailWith(cl, "neither the CL owner nor a committer") 362 }) 363 }) 364 365 Convey("w/ dependencies", func() { 366 // if triggerer is not the owner, but a 367 // committer/dry-runner, then untrusted deps 368 // should be checked. 369 tr, owner := "t@example.org", "o@example.org" 370 cl, _ := addCL(tr, owner, m) 371 372 dep1 := addDep(cl, "dep_owner1@example.org") 373 dep2 := addDep(cl, "dep_owner2@example.org") 374 dep1URL := dep1.ExternalID.MustURL() 375 dep2URL := dep2.ExternalID.MustURL() 376 377 testCases := func() { 378 Convey("untrusted", func() { 379 res := mustFailWith(cl, untrustedDeps) 380 So(res.Failure(cl), ShouldContainSubstring, dep1URL) 381 So(res.Failure(cl), ShouldContainSubstring, dep2URL) 382 // if the deps have no submit requirements, the rejection message 383 // shouldn't contain a warning for suspicious CLs. 384 So(res.Failure(cl), ShouldNotContainSubstring, untrustedDepsSuspicious) 385 386 Convey("with TrustDryRunnerDeps", func() { 387 setTrustDryRunnerDeps(true) 388 mustFailWith(cl, untrustedDepsTrustDryRunnerDeps) 389 }) 390 391 Convey("but dep2 satisfies all the SubmitRequirements", func() { 392 naReq(dep1, "Code-Review") 393 unsatisfiedReq(dep1, "Code-Owner") 394 satisfiedReq(dep2, "Code-Review") 395 satisfiedReq(dep2, "Code-Owner") 396 res := mustFailWith(cl, untrustedDeps) 397 So(res.Failure(cl), ShouldContainSubstring, fmt.Sprintf(""+ 398 "- %s: not submittable, although submit requirements `Code-Review` and `Code-Owner` are satisfied", 399 dep2URL, 400 )) 401 So(res.Failure(cl), ShouldContainSubstring, untrustedDepsSuspicious) 402 }) 403 404 Convey("because all the deps have unsatisfied requirements", func() { 405 dep3 := addDep(cl, "dep_owner3@example.org") 406 dep3URL := dep3.ExternalID.MustURL() 407 408 unsatisfiedReq(dep1, "Code-Review") 409 unsatisfiedReq(dep2, "Code-Review") 410 unsatisfiedReq(dep2, "Code-Owner") 411 unsatisfiedReq(dep3, "Code-Review") 412 unsatisfiedReq(dep3, "Code-Owner") 413 unsatisfiedReq(dep3, "Code-Quiz") 414 415 res := mustFailWith(cl, untrustedDeps) 416 So(res.Failure(cl), ShouldNotContainSubstring, untrustedDepsSuspicious) 417 So(res.Failure(cl), ShouldContainSubstring, 418 dep1URL+": not satisfying the `Code-Review` submit requirement") 419 So(res.Failure(cl), ShouldContainSubstring, 420 dep2URL+": not satisfying the `Code-Review` and `Code-Owner` submit requirement") 421 So(res.Failure(cl), ShouldContainSubstring, 422 dep3URL+": not satisfying the `Code-Review`, `Code-Owner`, and `Code-Quiz` submit requirement") 423 }) 424 }) 425 Convey("trusted because it's apart of the Run", func() { 426 cls = append(cls, dep1, dep2) 427 trs = append(trs, &run.Trigger{Email: tr, Mode: string(m)}) 428 trs = append(trs, &run.Trigger{Email: tr, Mode: string(m)}) 429 mustOK() 430 }) 431 Convey("trusted because of submittable", func() { 432 markCLSubmittable(dep1) 433 markCLSubmittable(dep2) 434 mustOK() 435 }) 436 Convey("trusterd because they have been merged already", func() { 437 submitCL(dep1) 438 submitCL(dep2) 439 mustOK() 440 }) 441 Convey("trusted because the owner is a committer", func() { 442 addCommitter("dep_owner1@example.org") 443 addCommitter("dep_owner2@example.org") 444 mustOK() 445 }) 446 Convey("trusted because the owner is a dry-runner", func() { 447 addDryRunner("dep_owner1@example.org") 448 addDryRunner("dep_owner2@example.org") 449 450 // Not allowed without TrustDryRunnerDeps. 451 res := mustFailWith(cl, untrustedDeps) 452 So(res.Failure(cl), ShouldContainSubstring, dep1URL) 453 So(res.Failure(cl), ShouldContainSubstring, dep2URL) 454 455 setTrustDryRunnerDeps(true) 456 mustOK() 457 }) 458 Convey("a mix of untrusted and trusted deps", func() { 459 addCommitter("dep_owner1@example.org") 460 res := mustFailWith(cl, untrustedDeps) 461 So(res.Failure(cl), ShouldNotContainSubstring, dep1URL) 462 So(res.Failure(cl), ShouldContainSubstring, dep2URL) 463 }) 464 } 465 466 Convey("committer", func() { 467 addCommitter(tr) 468 testCases() 469 }) 470 471 Convey("dry-runner (with allow_non_owner_dry_runner)", func() { 472 addDryRunner(tr) 473 setAllowNonOwnerDryRunner(true) 474 testCases() 475 }) 476 }) 477 }) 478 479 Convey("mode == NewPatchsetRun", func() { 480 tr, owner := "t@example.org", "t@example.org" 481 cl, _ := addCL(tr, owner, run.NewPatchsetRun) 482 Convey("owner is disallowed", func() { 483 mustFailWith(cl, "CL owner is not in the allowlist.") 484 }) 485 Convey("owner is allowed", func() { 486 addNPRunner(owner) 487 mustOK() 488 }) 489 }) 490 491 Convey("mode is non standard mode", func() { 492 tr, owner := "t@example.org", "t@example.org" 493 cl, trigger := addCL(tr, owner, "CUSTOM_RUN") 494 trigger.ModeDefinition = &cfgpb.Mode{ 495 Name: "CUSTOM_RUN", 496 TriggeringLabel: "CUSTOM", 497 TriggeringValue: 1, 498 } 499 Convey("dry", func() { 500 trigger.ModeDefinition.CqLabelValue = 1 501 Convey("disallowed", func() { 502 mustFailWith(cl, "CV cannot start a Run for `%s` because the user is not a dry-runner", owner) 503 }) 504 Convey("allowed", func() { 505 addDryRunner(owner) 506 mustOK() 507 }) 508 }) 509 Convey("full", func() { 510 trigger.ModeDefinition.CqLabelValue = 2 511 markCLSubmittable(cl) 512 Convey("disallowed", func() { 513 mustFailWith(cl, "CV cannot start a Run for `%s` because the user is not a committer", owner) 514 }) 515 Convey("allowed", func() { 516 addCommitter(owner) 517 mustOK() 518 }) 519 }) 520 }) 521 522 Convey("multiple CLs", func() { 523 m := run.DryRun 524 tr, owner := "t@example.org", "t@example.org" 525 cl1, _ := addCL(tr, owner, m) 526 cl2, _ := addCL(tr, owner, m) 527 setAllowOwner(cfgpb.Verifiers_GerritCQAbility_DRY_RUN) 528 529 Convey("all CLs passed", func() { 530 markCLSubmittable(cl1) 531 markCLSubmittable(cl2) 532 mustOK() 533 }) 534 Convey("all CLs failed", func() { 535 mustFailWith(cl1, notSubmittable) 536 mustFailWith(cl2, notSubmittable) 537 }) 538 Convey("Some CLs failed", func() { 539 markCLSubmittable(cl1) 540 mustFailWith(cl1, "CV cannot start a Run due to errors in the following CL(s)") 541 mustFailWith(cl2, notSubmittable) 542 }) 543 }) 544 }) 545 }