go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/internal/realmsinternals/config_test.go (about) 1 // Copyright 2023 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 realmsinternals 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "testing" 22 "time" 23 24 "google.golang.org/protobuf/encoding/prototext" 25 "google.golang.org/protobuf/proto" 26 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 realmsconf "go.chromium.org/luci/common/proto/realms" 30 "go.chromium.org/luci/config" 31 "go.chromium.org/luci/config/cfgclient" 32 "go.chromium.org/luci/config/impl/memory" 33 "go.chromium.org/luci/gae/filter/txndefer" 34 gaemem "go.chromium.org/luci/gae/impl/memory" 35 "go.chromium.org/luci/gae/service/datastore" 36 "go.chromium.org/luci/server/auth" 37 "go.chromium.org/luci/server/auth/authtest" 38 "go.chromium.org/luci/server/auth/service/protocol" 39 "go.chromium.org/luci/server/tq" 40 41 "go.chromium.org/luci/auth_service/impl/info" 42 "go.chromium.org/luci/auth_service/impl/model" 43 44 . "github.com/smartystreets/goconvey/convey" 45 . "go.chromium.org/luci/common/testing/assertions" 46 ) 47 48 var ( 49 testCreatedTS = time.Date(2020, time.May, 4, 0, 0, 0, 0, time.UTC) 50 testModifiedTS = time.Date(2021, time.August, 16, 12, 20, 0, 0, time.UTC) 51 ) 52 53 func testAuthVersionedEntityMixin() model.AuthVersionedEntityMixin { 54 return model.AuthVersionedEntityMixin{ 55 ModifiedTS: testModifiedTS, 56 ModifiedBy: "user:test-modifier@example.com", 57 AuthDBRev: 1337, 58 AuthDBPrevRev: 1336, 59 } 60 } 61 62 func TestGetConfigs(t *testing.T) { 63 projectRealmsKey := func(ctx context.Context, project string) *datastore.Key { 64 return datastore.NewKey(ctx, "AuthProjectRealms", project, 0, model.RootKey(ctx)) 65 } 66 67 Convey("projects with config", t, func() { 68 ctx := gaemem.Use(context.Background()) 69 ctx = cfgclient.Use(ctx, &fakeCfgClient{}) 70 ctx = txndefer.FilterRDS(ctx) 71 err := datastore.Put(ctx, &model.AuthProjectRealms{ 72 AuthVersionedEntityMixin: testAuthVersionedEntityMixin(), 73 Kind: "AuthProjectRealms", 74 ID: "test", 75 Parent: model.RootKey(ctx), 76 Realms: []byte{}, 77 PermsRev: "123", 78 ConfigRev: "1234", 79 }) 80 So(err, ShouldBeNil) 81 err = datastore.Put(ctx, &model.AuthProjectRealmsMeta{ 82 Kind: "AuthProjectRealmsMeta", 83 ID: "meta", 84 Parent: projectRealmsKey(ctx, "test"), 85 PermsRev: "123", 86 ConfigRev: "1234", 87 ConfigDigest: "test-digest", 88 ModifiedTS: testModifiedTS, 89 }) 90 So(err, ShouldBeNil) 91 92 latestExpected := []*model.RealmsCfgRev{ 93 { 94 ProjectID: "@internal", 95 ConfigRev: testRevision, 96 ConfigDigest: testContentHash, 97 ConfigBody: []byte{}, 98 PermsRev: "", 99 }, 100 { 101 ProjectID: "test-project-a", 102 ConfigRev: testRevision, 103 ConfigDigest: testContentHash, 104 ConfigBody: []byte{}, 105 PermsRev: "", 106 }, 107 { 108 ProjectID: "test-project-d", 109 ConfigRev: testRevision, 110 ConfigDigest: testContentHash, 111 ConfigBody: []byte{}, 112 PermsRev: "", 113 }, 114 } 115 116 storedExpected := []*model.RealmsCfgRev{ 117 { 118 ProjectID: "test", 119 ConfigRev: "1234", 120 PermsRev: "123", 121 ConfigDigest: "test-digest", 122 }, 123 } 124 125 latest, stored, err := GetConfigs(ctx) 126 127 sortRevsByID(latest) 128 sortRevsByID(latestExpected) 129 130 So(err, ShouldBeNil) 131 So(latest, ShouldResemble, latestExpected) 132 So(stored, ShouldResemble, storedExpected) 133 }) 134 135 Convey("no projects with config", t, func() { 136 ctx := gaemem.Use(context.Background()) 137 ctx = cfgclient.Use(ctx, memory.New(map[config.Set]memory.Files{})) 138 ctx = txndefer.FilterRDS(ctx) 139 err := datastore.Put(ctx, &model.AuthProjectRealms{ 140 AuthVersionedEntityMixin: testAuthVersionedEntityMixin(), 141 Kind: "AuthProjectRealms", 142 ID: "test", 143 Parent: model.RootKey(ctx), 144 Realms: []byte{}, 145 PermsRev: "123", 146 ConfigRev: "1234", 147 }) 148 So(err, ShouldBeNil) 149 err = datastore.Put(ctx, &model.AuthProjectRealmsMeta{ 150 Kind: "AuthProjectRealmsMeta", 151 ID: "meta", 152 Parent: projectRealmsKey(ctx, "test"), 153 PermsRev: "123", 154 ConfigRev: "1234", 155 ConfigDigest: "test-digest", 156 ModifiedTS: testModifiedTS, 157 }) 158 So(err, ShouldBeNil) 159 _, _, err = GetConfigs(ctx) 160 So(err, ShouldErrLike, config.ErrNoConfig) 161 }) 162 163 Convey("no entity in ds", t, func() { 164 ctx := gaemem.Use(context.Background()) 165 ctx = cfgclient.Use(ctx, &fakeCfgClient{}) 166 datastore.GetTestable(ctx).AutoIndex(true) 167 datastore.GetTestable(ctx).Consistent(true) 168 _, stored, err := GetConfigs(ctx) 169 So(err, ShouldBeNil) 170 So(stored, ShouldHaveLength, 0) 171 }) 172 } 173 174 func TestRealmsExpansion(t *testing.T) { 175 t.Parallel() 176 177 binding := func(roleName string, principals []string, restrictions map[string][]string) *realmsconf.Binding { 178 conds := []*realmsconf.Condition{} 179 for attr, vals := range restrictions { 180 conds = append(conds, &realmsconf.Condition{ 181 Op: &realmsconf.Condition_Restrict{ 182 Restrict: &realmsconf.Condition_AttributeRestriction{ 183 Attribute: attr, 184 Values: vals, 185 }, 186 }, 187 }) 188 } 189 return &realmsconf.Binding{ 190 Role: roleName, 191 Principals: principals, 192 Conditions: conds, 193 } 194 } 195 196 Convey("ExpandRealms works", t, func() { 197 Convey("completely empty", func() { 198 permDB := testPermissionsDB(false) 199 actualRealms, err := ExpandRealms(permDB, "p", nil) 200 201 expectedRealms := &protocol.Realms{ 202 Realms: []*protocol.Realm{ 203 { 204 Name: "p:@root", 205 }, 206 }, 207 } 208 So(err, ShouldBeNil) 209 So(actualRealms, ShouldResembleProto, expectedRealms) 210 }) 211 212 Convey("empty realm", func() { 213 permDB := testPermissionsDB(false) 214 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 215 Realms: []*realmsconf.Realm{ 216 { 217 Name: "r2", 218 }, 219 { 220 Name: "r1", 221 }, 222 }, 223 }) 224 225 expectedRealms := &protocol.Realms{ 226 Realms: []*protocol.Realm{ 227 { 228 Name: "p:@root", 229 }, 230 { 231 Name: "p:r1", 232 }, 233 { 234 Name: "p:r2", 235 }, 236 }, 237 } 238 So(err, ShouldBeNil) 239 So(actualRealms, ShouldResembleProto, expectedRealms) 240 }) 241 242 Convey("simple bindings", func() { 243 permDB := testPermissionsDB(false) 244 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 245 Realms: []*realmsconf.Realm{ 246 { 247 Name: "r", 248 Bindings: []*realmsconf.Binding{ 249 binding("role/dev.a", []string{"group:gr1", "group:gr3"}, nil), 250 binding("role/dev.b", []string{"group:gr2", "group:gr3"}, nil), 251 binding("role/dev.all", []string{"group:gr4"}, nil), 252 }, 253 }, 254 }, 255 }) 256 257 expectedRealms := &protocol.Realms{ 258 Permissions: []*protocol.Permission{ 259 {Name: "luci.dev.p1"}, 260 {Name: "luci.dev.p2"}, 261 {Name: "luci.dev.p3"}, 262 }, 263 Realms: []*protocol.Realm{ 264 { 265 Name: "p:@root", 266 }, 267 { 268 Name: "p:r", 269 Bindings: []*protocol.Binding{ 270 { 271 Permissions: []uint32{0, 1}, 272 Principals: []string{"group:gr1"}, 273 }, 274 { 275 Permissions: []uint32{0, 1, 2}, 276 Principals: []string{"group:gr3", "group:gr4"}, 277 }, 278 { 279 Permissions: []uint32{1, 2}, 280 Principals: []string{"group:gr2"}, 281 }, 282 }, 283 }, 284 }, 285 } 286 So(err, ShouldBeNil) 287 So(actualRealms, ShouldResembleProto, expectedRealms) 288 }) 289 290 Convey("simple bindings with conditions", func() { 291 permDB := testPermissionsDB(false) 292 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 293 Realms: []*realmsconf.Realm{ 294 { 295 Name: "r", 296 Bindings: []*realmsconf.Binding{ 297 binding("role/dev.a", []string{"group:gr1", "group:gr3"}, nil), 298 binding("role/dev.b", []string{"group:gr2", "group:gr3"}, nil), 299 binding("role/dev.all", []string{"group:gr4"}, nil), 300 binding("role/dev.a", []string{"group:gr1"}, map[string][]string{"a1": {"1", "2"}}), 301 binding("role/dev.a", []string{"group:gr1"}, map[string][]string{"a1": {"1", "2"}}), 302 binding("role/dev.a", []string{"group:gr2"}, map[string][]string{"a1": {"2", "1"}}), 303 binding("role/dev.b", []string{"group:gr2"}, map[string][]string{"a1": {"1", "2"}}), 304 binding("role/dev.b", []string{"group:gr2"}, map[string][]string{"a2": {"1", "2"}}), 305 }, 306 }, 307 }, 308 }) 309 310 expectedRealms := &protocol.Realms{ 311 Permissions: []*protocol.Permission{ 312 {Name: "luci.dev.p1"}, 313 {Name: "luci.dev.p2"}, 314 {Name: "luci.dev.p3"}, 315 }, 316 Conditions: []*protocol.Condition{ 317 { 318 Op: &protocol.Condition_Restrict{ 319 Restrict: &protocol.Condition_AttributeRestriction{ 320 Attribute: "a1", 321 Values: []string{"1", "2"}, 322 }, 323 }, 324 }, 325 { 326 Op: &protocol.Condition_Restrict{ 327 Restrict: &protocol.Condition_AttributeRestriction{ 328 Attribute: "a2", 329 Values: []string{"1", "2"}, 330 }, 331 }, 332 }, 333 }, 334 Realms: []*protocol.Realm{ 335 { 336 Name: "p:@root", 337 }, 338 { 339 Name: "p:r", 340 Bindings: []*protocol.Binding{ 341 { 342 Permissions: []uint32{0, 1}, 343 Principals: []string{"group:gr1"}, 344 }, 345 { 346 Permissions: []uint32{0, 1}, 347 Principals: []string{"group:gr1"}, 348 Conditions: []uint32{0}, 349 }, 350 { 351 Permissions: []uint32{0, 1, 2}, 352 Principals: []string{"group:gr3", "group:gr4"}, 353 }, 354 { 355 Permissions: []uint32{0, 1, 2}, 356 Principals: []string{"group:gr2"}, 357 Conditions: []uint32{0}, 358 }, 359 { 360 Permissions: []uint32{1, 2}, 361 Principals: []string{"group:gr2"}, 362 }, 363 { 364 Permissions: []uint32{1, 2}, 365 Principals: []string{"group:gr2"}, 366 Conditions: []uint32{1}, 367 }, 368 }, 369 }, 370 }, 371 } 372 So(err, ShouldBeNil) 373 So(actualRealms, ShouldResembleProto, expectedRealms) 374 }) 375 376 Convey("custom root", func() { 377 permDB := testPermissionsDB(false) 378 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 379 Realms: []*realmsconf.Realm{ 380 { 381 Name: "@root", 382 Bindings: []*realmsconf.Binding{ 383 binding("role/dev.all", []string{"group:gr4"}, nil), 384 }, 385 }, 386 { 387 Name: "r", 388 Bindings: []*realmsconf.Binding{ 389 binding("role/dev.a", []string{"group:gr1", "group:gr3"}, nil), 390 binding("role/dev.b", []string{"group:gr2", "group:gr3"}, nil), 391 }, 392 }, 393 }, 394 }) 395 396 expectedRealms := &protocol.Realms{ 397 Permissions: []*protocol.Permission{ 398 {Name: "luci.dev.p1"}, 399 {Name: "luci.dev.p2"}, 400 {Name: "luci.dev.p3"}, 401 }, 402 Realms: []*protocol.Realm{ 403 { 404 Name: "p:@root", 405 Bindings: []*protocol.Binding{ 406 { 407 Permissions: []uint32{0, 1, 2}, 408 Principals: []string{"group:gr4"}, 409 }, 410 }, 411 }, 412 { 413 Name: "p:r", 414 Bindings: []*protocol.Binding{ 415 { 416 Permissions: []uint32{0, 1}, 417 Principals: []string{"group:gr1"}, 418 }, 419 { 420 Permissions: []uint32{0, 1, 2}, 421 Principals: []string{"group:gr3", "group:gr4"}, 422 }, 423 { 424 Permissions: []uint32{1, 2}, 425 Principals: []string{"group:gr2"}, 426 }, 427 }, 428 }, 429 }, 430 } 431 So(err, ShouldBeNil) 432 So(actualRealms, ShouldResembleProto, expectedRealms) 433 }) 434 435 Convey("realm inheritance", func() { 436 permDB := testPermissionsDB(false) 437 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 438 Realms: []*realmsconf.Realm{ 439 { 440 Name: "@root", 441 Bindings: []*realmsconf.Binding{ 442 binding("role/dev.all", []string{"group:gr4"}, nil), 443 }, 444 }, 445 { 446 Name: "r1", 447 Bindings: []*realmsconf.Binding{ 448 binding("role/dev.a", []string{"group:gr1", "group:gr3"}, nil), 449 }, 450 }, 451 { 452 Name: "r2", 453 Bindings: []*realmsconf.Binding{ 454 binding("role/dev.b", []string{"group:gr2", "group:gr3"}, nil), 455 }, 456 Extends: []string{ 457 "r1", 458 "@root", 459 }, 460 }, 461 }, 462 }) 463 464 expectedRealms := &protocol.Realms{ 465 Permissions: []*protocol.Permission{ 466 {Name: "luci.dev.p1"}, 467 {Name: "luci.dev.p2"}, 468 {Name: "luci.dev.p3"}, 469 }, 470 Realms: []*protocol.Realm{ 471 { 472 Name: "p:@root", 473 Bindings: []*protocol.Binding{ 474 { 475 Permissions: []uint32{0, 1, 2}, 476 Principals: []string{"group:gr4"}, 477 }, 478 }, 479 }, 480 { 481 Name: "p:r1", 482 Bindings: []*protocol.Binding{ 483 { 484 Permissions: []uint32{0, 1}, 485 Principals: []string{"group:gr1", "group:gr3"}, 486 }, 487 { 488 Permissions: []uint32{0, 1, 2}, 489 Principals: []string{"group:gr4"}, 490 }, 491 }, 492 }, 493 { 494 Name: "p:r2", 495 Bindings: []*protocol.Binding{ 496 { 497 Permissions: []uint32{0, 1}, 498 Principals: []string{"group:gr1"}, 499 }, 500 { 501 Permissions: []uint32{0, 1, 2}, 502 Principals: []string{"group:gr3", "group:gr4"}, 503 }, 504 { 505 Permissions: []uint32{1, 2}, 506 Principals: []string{"group:gr2"}, 507 }, 508 }, 509 }, 510 }, 511 } 512 So(err, ShouldBeNil) 513 So(actualRealms, ShouldResembleProto, expectedRealms) 514 }) 515 516 Convey("realm inheritance with conditions", func() { 517 permDB := testPermissionsDB(false) 518 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 519 Realms: []*realmsconf.Realm{ 520 { 521 Name: "@root", 522 Bindings: []*realmsconf.Binding{ 523 binding("role/dev.all", []string{"group:gr4"}, nil), 524 binding("role/dev.a", []string{"group:gr5"}, map[string][]string{"a1": {"1"}}), 525 }, 526 }, 527 { 528 Name: "r1", 529 Bindings: []*realmsconf.Binding{ 530 binding("role/dev.a", []string{"group:gr1", "group:gr3"}, nil), 531 binding("role/dev.a", []string{"group:gr6"}, map[string][]string{"a1": {"1"}}), 532 }, 533 }, 534 { 535 Name: "r2", 536 Bindings: []*realmsconf.Binding{ 537 binding("role/dev.b", []string{"group:gr2", "group:gr3"}, nil), 538 binding("role/dev.a", []string{"group:gr1", "group:gr6", "group:gr7"}, map[string][]string{"a1": {"1"}}), 539 }, 540 Extends: []string{ 541 "r1", 542 "@root", 543 }, 544 }, 545 }, 546 }) 547 548 expectedRealms := &protocol.Realms{ 549 Permissions: []*protocol.Permission{ 550 {Name: "luci.dev.p1"}, 551 {Name: "luci.dev.p2"}, 552 {Name: "luci.dev.p3"}, 553 }, 554 Conditions: []*protocol.Condition{ 555 { 556 Op: &protocol.Condition_Restrict{ 557 Restrict: &protocol.Condition_AttributeRestriction{ 558 Attribute: "a1", 559 Values: []string{"1"}, 560 }, 561 }, 562 }, 563 }, 564 Realms: []*protocol.Realm{ 565 { 566 Name: "p:@root", 567 Bindings: []*protocol.Binding{ 568 { 569 Permissions: []uint32{0, 1}, 570 Principals: []string{"group:gr5"}, 571 Conditions: []uint32{0}, 572 }, 573 { 574 Permissions: []uint32{0, 1, 2}, 575 Principals: []string{"group:gr4"}, 576 }, 577 }, 578 }, 579 { 580 Name: "p:r1", 581 Bindings: []*protocol.Binding{ 582 { 583 Permissions: []uint32{0, 1}, 584 Principals: []string{"group:gr1", "group:gr3"}, 585 }, 586 { 587 Permissions: []uint32{0, 1}, 588 Principals: []string{"group:gr5", "group:gr6"}, 589 Conditions: []uint32{0}, 590 }, 591 { 592 Permissions: []uint32{0, 1, 2}, 593 Principals: []string{"group:gr4"}, 594 }, 595 }, 596 }, 597 { 598 Name: "p:r2", 599 Bindings: []*protocol.Binding{ 600 { 601 Permissions: []uint32{0, 1}, 602 Principals: []string{"group:gr1"}, 603 }, 604 { 605 Permissions: []uint32{0, 1}, 606 Principals: []string{ 607 "group:gr1", 608 "group:gr5", 609 "group:gr6", 610 "group:gr7", 611 }, 612 Conditions: []uint32{0}, 613 }, 614 { 615 Permissions: []uint32{0, 1, 2}, 616 Principals: []string{"group:gr3", "group:gr4"}, 617 }, 618 { 619 Permissions: []uint32{1, 2}, 620 Principals: []string{"group:gr2"}, 621 }, 622 }, 623 }, 624 }, 625 } 626 So(err, ShouldBeNil) 627 So(actualRealms, ShouldResembleProto, expectedRealms) 628 }) 629 630 Convey("custom roles", func() { 631 permDB := testPermissionsDB(false) 632 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 633 CustomRoles: []*realmsconf.CustomRole{ 634 { 635 Name: "customRole/r1", 636 Extends: []string{"role/dev.a"}, 637 Permissions: []string{"luci.dev.p4"}, 638 }, 639 { 640 Name: "customRole/r2", 641 Extends: []string{"customRole/r1", "role/dev.b"}, 642 }, 643 { 644 Name: "customRole/r3", 645 Permissions: []string{"luci.dev.p5"}, 646 }, 647 }, 648 Realms: []*realmsconf.Realm{ 649 { 650 Name: "r", 651 Bindings: []*realmsconf.Binding{ 652 binding("customRole/r1", []string{"group:gr1", "group:gr3"}, nil), 653 binding("customRole/r2", []string{"group:gr2", "group:gr3"}, nil), 654 binding("customRole/r3", []string{"group:gr5"}, nil), 655 }, 656 }, 657 }, 658 }) 659 660 expectedRealms := &protocol.Realms{ 661 Permissions: []*protocol.Permission{ 662 {Name: "luci.dev.p1"}, 663 {Name: "luci.dev.p2"}, 664 {Name: "luci.dev.p3"}, 665 {Name: "luci.dev.p4"}, 666 {Name: "luci.dev.p5"}, 667 }, 668 Realms: []*protocol.Realm{ 669 { 670 Name: "p:@root", 671 }, 672 { 673 Name: "p:r", 674 Bindings: []*protocol.Binding{ 675 { 676 Permissions: []uint32{0, 1, 2, 3}, 677 Principals: []string{"group:gr2", "group:gr3"}, 678 }, 679 { 680 Permissions: []uint32{0, 1, 3}, 681 Principals: []string{"group:gr1"}, 682 }, 683 { 684 Permissions: []uint32{4}, 685 Principals: []string{"group:gr5"}, 686 }, 687 }, 688 }, 689 }, 690 } 691 So(err, ShouldBeNil) 692 So(actualRealms, ShouldResembleProto, expectedRealms) 693 }) 694 695 Convey("implicit root bindings with no root", func() { 696 permDB := testPermissionsDB(true) 697 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 698 Realms: []*realmsconf.Realm{ 699 { 700 Name: "r", 701 Bindings: []*realmsconf.Binding{ 702 binding("role/dev.a", []string{"group:gr"}, nil), 703 }, 704 }, 705 }, 706 }) 707 708 expectedRealms := &protocol.Realms{ 709 Conditions: []*protocol.Condition{ 710 { 711 Op: &protocol.Condition_Restrict{ 712 Restrict: &protocol.Condition_AttributeRestriction{ 713 Attribute: "root", 714 Values: []string{"yes"}, 715 }, 716 }, 717 }, 718 }, 719 Permissions: []*protocol.Permission{ 720 {Name: "luci.dev.implicitRoot"}, 721 {Name: "luci.dev.p1"}, 722 {Name: "luci.dev.p2"}, 723 }, 724 Realms: []*protocol.Realm{ 725 { 726 Name: "p:@root", 727 Bindings: []*protocol.Binding{ 728 { 729 Permissions: []uint32{0}, 730 Principals: []string{"project:p"}, 731 }, 732 { 733 Permissions: []uint32{0}, 734 Conditions: []uint32{0}, 735 Principals: []string{"group:root"}, 736 }, 737 }, 738 }, 739 { 740 Name: "p:r", 741 Bindings: []*protocol.Binding{ 742 { 743 Permissions: []uint32{0}, 744 Principals: []string{"project:p"}, 745 }, 746 { 747 Permissions: []uint32{0}, 748 Conditions: []uint32{0}, 749 Principals: []string{"group:root"}, 750 }, 751 { 752 Permissions: []uint32{1, 2}, 753 Principals: []string{"group:gr"}, 754 }, 755 }, 756 }, 757 }, 758 } 759 760 So(err, ShouldBeNil) 761 So(actualRealms, ShouldResembleProto, expectedRealms) 762 }) 763 764 Convey("implicit root bindings with root", func() { 765 permDB := testPermissionsDB(true) 766 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 767 Realms: []*realmsconf.Realm{ 768 { 769 Name: "@root", 770 Bindings: []*realmsconf.Binding{ 771 binding("role/dev.a", []string{"group:gr1"}, nil), 772 }, 773 }, 774 { 775 Name: "r", 776 Bindings: []*realmsconf.Binding{ 777 binding("role/dev.a", []string{"group:gr2"}, nil), 778 binding("role/dev.a", []string{"group:gr2"}, map[string][]string{"root": {"yes"}}), 779 binding("role/dev.a", []string{"group:gr3"}, map[string][]string{"a1": {"1"}}), 780 }, 781 }, 782 }, 783 }) 784 785 expectedRealms := &protocol.Realms{ 786 Conditions: []*protocol.Condition{ 787 { 788 Op: &protocol.Condition_Restrict{ 789 Restrict: &protocol.Condition_AttributeRestriction{ 790 Attribute: "a1", 791 Values: []string{"1"}, 792 }, 793 }, 794 }, 795 { 796 Op: &protocol.Condition_Restrict{ 797 Restrict: &protocol.Condition_AttributeRestriction{ 798 Attribute: "root", 799 Values: []string{"yes"}, 800 }, 801 }, 802 }, 803 }, 804 Permissions: []*protocol.Permission{ 805 {Name: "luci.dev.implicitRoot"}, 806 {Name: "luci.dev.p1"}, 807 {Name: "luci.dev.p2"}, 808 }, 809 Realms: []*protocol.Realm{ 810 { 811 Name: "p:@root", 812 Bindings: []*protocol.Binding{ 813 { 814 Permissions: []uint32{0}, 815 Principals: []string{"project:p"}, 816 }, 817 { 818 Conditions: []uint32{1}, 819 Permissions: []uint32{0}, 820 Principals: []string{"group:root"}, 821 }, 822 { 823 Permissions: []uint32{1, 2}, 824 Principals: []string{"group:gr1"}, 825 }, 826 }, 827 }, 828 { 829 Name: "p:r", 830 Bindings: []*protocol.Binding{ 831 { 832 Permissions: []uint32{0}, 833 Principals: []string{"project:p"}, 834 }, 835 { 836 Conditions: []uint32{1}, 837 Permissions: []uint32{0}, 838 Principals: []string{"group:root"}, 839 }, 840 { 841 Permissions: []uint32{1, 2}, 842 Principals: []string{"group:gr1", "group:gr2"}, 843 }, 844 { 845 Conditions: []uint32{0}, 846 Permissions: []uint32{1, 2}, 847 Principals: []string{"group:gr3"}, 848 }, 849 { 850 Conditions: []uint32{1}, 851 Permissions: []uint32{1, 2}, 852 Principals: []string{"group:gr2"}, 853 }, 854 }, 855 }, 856 }, 857 } 858 859 So(err, ShouldBeNil) 860 So(actualRealms, ShouldResembleProto, expectedRealms) 861 }) 862 863 Convey("implicit root bindings in internal", func() { 864 permDB := testPermissionsDB(true) 865 actualRealms, err := ExpandRealms(permDB, "@internal", &realmsconf.RealmsCfg{ 866 Realms: []*realmsconf.Realm{ 867 { 868 Name: "r", 869 Bindings: []*realmsconf.Binding{ 870 binding("role/dev.a", []string{"group:gr"}, nil), 871 }, 872 }, 873 }, 874 }) 875 876 expectedRealms := &protocol.Realms{ 877 Permissions: []*protocol.Permission{ 878 {Name: "luci.dev.p1", Internal: true}, 879 {Name: "luci.dev.p2", Internal: true}, 880 }, 881 Realms: []*protocol.Realm{ 882 { 883 Name: "@internal:@root", 884 }, 885 { 886 Name: "@internal:r", 887 Bindings: []*protocol.Binding{ 888 { 889 Permissions: []uint32{0, 1}, 890 Principals: []string{"group:gr"}, 891 }, 892 }, 893 }, 894 }, 895 } 896 897 So(err, ShouldBeNil) 898 So(actualRealms, ShouldResembleProto, expectedRealms) 899 }) 900 901 Convey("enforce in service", func() { 902 permDB := testPermissionsDB(false) 903 actualRealms, err := ExpandRealms(permDB, "p", &realmsconf.RealmsCfg{ 904 Realms: []*realmsconf.Realm{ 905 { 906 Name: "@root", 907 EnforceInService: []string{"a"}, 908 }, 909 { 910 Name: "r1", 911 }, 912 { 913 Name: "r2", 914 EnforceInService: []string{"b"}, 915 }, 916 { 917 Name: "r3", 918 EnforceInService: []string{"c"}, 919 }, 920 { 921 Name: "r4", 922 Extends: []string{"r1", "r2", "r3"}, 923 EnforceInService: []string{"d"}, 924 }, 925 }, 926 }) 927 928 expectedRealms := &protocol.Realms{ 929 Realms: []*protocol.Realm{ 930 { 931 Name: "p:@root", 932 Data: &protocol.RealmData{ 933 EnforceInService: []string{"a"}, 934 }, 935 }, 936 { 937 Name: "p:r1", 938 Data: &protocol.RealmData{ 939 EnforceInService: []string{"a"}, 940 }, 941 }, 942 { 943 Name: "p:r2", 944 Data: &protocol.RealmData{ 945 EnforceInService: []string{"a", "b"}, 946 }, 947 }, 948 { 949 Name: "p:r3", 950 Data: &protocol.RealmData{ 951 EnforceInService: []string{"a", "c"}, 952 }, 953 }, 954 { 955 Name: "p:r4", 956 Data: &protocol.RealmData{ 957 EnforceInService: []string{"a", "b", "c", "d"}, 958 }, 959 }, 960 }, 961 } 962 So(err, ShouldBeNil) 963 So(actualRealms, ShouldResembleProto, expectedRealms) 964 }) 965 }) 966 } 967 968 func TestUpdateRealms(t *testing.T) { 969 t.Parallel() 970 971 simpleProjectRealm := func(ctx context.Context, projectName string, expectedRealmsBody []byte, authDBRev int) *model.AuthProjectRealms { 972 return &model.AuthProjectRealms{ 973 AuthVersionedEntityMixin: model.AuthVersionedEntityMixin{ 974 ModifiedTS: testCreatedTS, 975 ModifiedBy: "user:someone@example.com", 976 AuthDBRev: int64(authDBRev), 977 AuthDBPrevRev: int64(authDBRev - 1), 978 }, 979 Kind: "AuthProjectRealms", 980 ID: fmt.Sprintf("test-project-%s", projectName), 981 Parent: model.RootKey(ctx), 982 Realms: expectedRealmsBody, 983 ConfigRev: testRevision, 984 PermsRev: "permissions.cfg:123", 985 } 986 } 987 988 simpleProjectRealmMeta := func(ctx context.Context, projectName string) *model.AuthProjectRealmsMeta { 989 return &model.AuthProjectRealmsMeta{ 990 Kind: "AuthProjectRealmsMeta", 991 ID: "meta", 992 Parent: datastore.NewKey(ctx, "AuthProjectRealms", fmt.Sprintf("test-project-%s", projectName), 0, model.RootKey(ctx)), 993 ConfigRev: testRevision, 994 ConfigDigest: testContentHash, 995 ModifiedTS: testCreatedTS, 996 PermsRev: "permissions.cfg:123", 997 } 998 } 999 1000 Convey("testing updating realms", t, func() { 1001 ctx := auth.WithState(gaemem.Use(context.Background()), &authtest.FakeState{ 1002 Identity: "user:someone@example.com", 1003 }) 1004 ctx = clock.Set(ctx, testclock.New(testCreatedTS)) 1005 ctx = info.SetImageVersion(ctx, "test-version") 1006 ctx, taskScheduler := tq.TestingContext(txndefer.FilterRDS(ctx), nil) 1007 1008 Convey("works", func() { 1009 Convey("simple config 1 entry", func() { 1010 configBody, _ := prototext.Marshal(&realmsconf.RealmsCfg{ 1011 Realms: []*realmsconf.Realm{ 1012 { 1013 Name: "test-realm", 1014 }, 1015 }, 1016 }) 1017 1018 revs := []*model.RealmsCfgRev{ 1019 { 1020 ProjectID: "test-project-a", 1021 ConfigRev: testRevision, 1022 ConfigDigest: testContentHash, 1023 ConfigBody: configBody, 1024 }, 1025 } 1026 err := UpdateRealms(ctx, testPermissionsDB(false), revs, false, "latest config") 1027 So(err, ShouldBeNil) 1028 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 1029 1030 expectedRealmsBody, _ := proto.Marshal(&protocol.Realms{ 1031 Realms: []*protocol.Realm{ 1032 { 1033 Name: "test-project-a:@root", 1034 }, 1035 { 1036 Name: "test-project-a:test-realm", 1037 }, 1038 }, 1039 }) 1040 1041 fetchedPRealms, err := model.GetAuthProjectRealms(ctx, "test-project-a") 1042 So(err, ShouldBeNil) 1043 So(fetchedPRealms, ShouldResemble, simpleProjectRealm(ctx, "a", expectedRealmsBody, 1)) 1044 1045 fetchedPRealmMeta, err := model.GetAuthProjectRealmsMeta(ctx, "test-project-a") 1046 So(err, ShouldBeNil) 1047 So(fetchedPRealmMeta, ShouldResemble, simpleProjectRealmMeta(ctx, "a")) 1048 }) 1049 1050 Convey("updating project entry with config changes", func() { 1051 cfgBody, _ := prototext.Marshal(&realmsconf.RealmsCfg{ 1052 Realms: []*realmsconf.Realm{ 1053 { 1054 Name: "test-realm", 1055 }, 1056 }, 1057 }) 1058 1059 revs := []*model.RealmsCfgRev{ 1060 { 1061 ProjectID: "test-project-a", 1062 ConfigRev: testRevision, 1063 ConfigDigest: testContentHash, 1064 ConfigBody: cfgBody, 1065 }, 1066 } 1067 So(UpdateRealms(ctx, testPermissionsDB(false), revs, false, "latest config"), ShouldBeNil) 1068 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 1069 1070 expectedRealmsBody, _ := proto.Marshal(&protocol.Realms{ 1071 Realms: []*protocol.Realm{ 1072 { 1073 Name: "test-project-a:@root", 1074 }, 1075 { 1076 Name: "test-project-a:test-realm", 1077 }, 1078 }, 1079 }) 1080 1081 fetchedPRealms, err := model.GetAuthProjectRealms(ctx, "test-project-a") 1082 So(err, ShouldBeNil) 1083 So(fetchedPRealms, ShouldResemble, simpleProjectRealm(ctx, "a", expectedRealmsBody, 1)) 1084 1085 fetchedPRealmMeta, err := model.GetAuthProjectRealmsMeta(ctx, "test-project-a") 1086 So(err, ShouldBeNil) 1087 So(fetchedPRealmMeta, ShouldResemble, simpleProjectRealmMeta(ctx, "a")) 1088 1089 cfgBody, _ = prototext.Marshal(&realmsconf.RealmsCfg{ 1090 Realms: []*realmsconf.Realm{ 1091 { 1092 Name: "test-realm", 1093 }, 1094 { 1095 Name: "test-realm-2", 1096 }, 1097 }, 1098 }) 1099 1100 revs = []*model.RealmsCfgRev{ 1101 { 1102 ProjectID: "test-project-a", 1103 ConfigRev: testRevision, 1104 ConfigDigest: testContentHash, 1105 ConfigBody: cfgBody, 1106 }, 1107 } 1108 So(UpdateRealms(ctx, testPermissionsDB(false), revs, false, "latest config"), ShouldBeNil) 1109 So(taskScheduler.Tasks(), ShouldHaveLength, 4) 1110 1111 expectedRealmsBody, _ = proto.Marshal(&protocol.Realms{ 1112 Realms: []*protocol.Realm{ 1113 { 1114 Name: "test-project-a:@root", 1115 }, 1116 { 1117 Name: "test-project-a:test-realm", 1118 }, 1119 { 1120 Name: "test-project-a:test-realm-2", 1121 }, 1122 }, 1123 }) 1124 1125 fetchedPRealms, err = model.GetAuthProjectRealms(ctx, "test-project-a") 1126 So(err, ShouldBeNil) 1127 So(fetchedPRealms, ShouldResemble, simpleProjectRealm(ctx, "a", expectedRealmsBody, 2)) 1128 1129 fetchedPRealmMeta, err = model.GetAuthProjectRealmsMeta(ctx, "test-project-a") 1130 So(err, ShouldBeNil) 1131 So(fetchedPRealmMeta, ShouldResemble, simpleProjectRealmMeta(ctx, "a")) 1132 }) 1133 1134 Convey("updating many projects", func() { 1135 cfgBody1, _ := prototext.Marshal(&realmsconf.RealmsCfg{ 1136 Realms: []*realmsconf.Realm{ 1137 { 1138 Name: "test-realm", 1139 }, 1140 { 1141 Name: "test-realm-2", 1142 }, 1143 }, 1144 }) 1145 1146 cfgBody2, _ := prototext.Marshal(&realmsconf.RealmsCfg{ 1147 Realms: []*realmsconf.Realm{ 1148 { 1149 Name: "test-realm", 1150 }, 1151 { 1152 Name: "test-realm-3", 1153 }, 1154 }, 1155 }) 1156 1157 revs := []*model.RealmsCfgRev{ 1158 { 1159 ProjectID: "test-project-a", 1160 ConfigRev: testRevision, 1161 ConfigDigest: testContentHash, 1162 ConfigBody: cfgBody1, 1163 }, 1164 { 1165 ProjectID: "test-project-b", 1166 ConfigRev: testRevision, 1167 ConfigDigest: testContentHash, 1168 ConfigBody: cfgBody2, 1169 }, 1170 } 1171 So(UpdateRealms(ctx, testPermissionsDB(false), revs, false, "latest config"), ShouldBeNil) 1172 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 1173 1174 expectedRealmsBodyA, _ := proto.Marshal(&protocol.Realms{ 1175 Realms: []*protocol.Realm{ 1176 { 1177 Name: "test-project-a:@root", 1178 }, 1179 { 1180 Name: "test-project-a:test-realm", 1181 }, 1182 { 1183 Name: "test-project-a:test-realm-2", 1184 }, 1185 }, 1186 }) 1187 1188 expectedRealmsBodyB, _ := proto.Marshal(&protocol.Realms{ 1189 Realms: []*protocol.Realm{ 1190 { 1191 Name: "test-project-b:@root", 1192 }, 1193 { 1194 Name: "test-project-b:test-realm", 1195 }, 1196 { 1197 Name: "test-project-b:test-realm-3", 1198 }, 1199 }, 1200 }) 1201 1202 fetchedPRealmsA, err := model.GetAuthProjectRealms(ctx, "test-project-a") 1203 So(err, ShouldBeNil) 1204 So(fetchedPRealmsA, ShouldResemble, simpleProjectRealm(ctx, "a", expectedRealmsBodyA, 1)) 1205 1206 fetchedPRealmsB, err := model.GetAuthProjectRealms(ctx, "test-project-b") 1207 So(err, ShouldBeNil) 1208 So(fetchedPRealmsB, ShouldResemble, simpleProjectRealm(ctx, "b", expectedRealmsBodyB, 1)) 1209 1210 fetchedPRealmMetaA, err := model.GetAuthProjectRealmsMeta(ctx, "test-project-a") 1211 So(err, ShouldBeNil) 1212 So(fetchedPRealmMetaA, ShouldResemble, simpleProjectRealmMeta(ctx, "a")) 1213 1214 fetchedPRealmMetaB, err := model.GetAuthProjectRealmsMeta(ctx, "test-project-b") 1215 So(err, ShouldBeNil) 1216 So(fetchedPRealmMetaB, ShouldResemble, simpleProjectRealmMeta(ctx, "b")) 1217 }) 1218 }) 1219 }) 1220 } 1221 1222 func TestCheckConfigChanges(t *testing.T) { 1223 t.Parallel() 1224 1225 Convey("testing realms config changes", t, func() { 1226 ctx := auth.WithState(gaemem.Use(context.Background()), &authtest.FakeState{ 1227 Identity: "user:someone@example.com", 1228 }) 1229 ctx = clock.Set(ctx, testclock.New(testCreatedTS)) 1230 ctx = info.SetImageVersion(ctx, "test-version") 1231 ctx, taskScheduler := tq.TestingContext(txndefer.FilterRDS(ctx), nil) 1232 1233 permsDB := testPermissionsDB(false) 1234 configBody, _ := prototext.Marshal(&realmsconf.RealmsCfg{ 1235 Realms: []*realmsconf.Realm{ 1236 { 1237 Name: "test-realm", 1238 }, 1239 }, 1240 }) 1241 1242 // makeFetchedCfgRev returns a RealmsCfgRev for the project, 1243 // with only the fields that would be populated when fetching it 1244 // from LUCI Config. 1245 // from stored info. 1246 makeFetchedCfgRev := func(projectID string) *model.RealmsCfgRev { 1247 return &model.RealmsCfgRev{ 1248 ProjectID: projectID, 1249 ConfigRev: testRevision, 1250 ConfigDigest: testContentHash, 1251 ConfigBody: configBody, 1252 PermsRev: "", 1253 } 1254 } 1255 1256 // makeStoredCfgRev returns a RealmsCfgRev for the project, 1257 // with only the fields that would be populated when creating it 1258 // from stored info. 1259 makeStoredCfgRev := func(projectID string) *model.RealmsCfgRev { 1260 return &model.RealmsCfgRev{ 1261 ProjectID: projectID, 1262 ConfigRev: testRevision, 1263 ConfigDigest: testContentHash, 1264 ConfigBody: []byte{}, 1265 PermsRev: permsDB.Rev, 1266 } 1267 } 1268 1269 // putProjectRealms stores an AuthProjectRealms into datastore 1270 // for the project. 1271 putProjectRealms := func(ctx context.Context, projectID string) error { 1272 return datastore.Put(ctx, &model.AuthProjectRealms{ 1273 AuthVersionedEntityMixin: testAuthVersionedEntityMixin(), 1274 Kind: "AuthProjectRealms", 1275 ID: projectID, 1276 Parent: model.RootKey(ctx), 1277 }) 1278 } 1279 1280 // runJobs is a helper function to execute callbacks. 1281 runJobs := func(jobs []func() error) bool { 1282 success := true 1283 for _, job := range jobs { 1284 if err := job(); err != nil { 1285 success = false 1286 } 1287 } 1288 return success 1289 } 1290 1291 Convey("no-op when up to date", func() { 1292 latest := []*model.RealmsCfgRev{ 1293 makeFetchedCfgRev("@internal"), 1294 makeFetchedCfgRev("test-project-a"), 1295 } 1296 stored := []*model.RealmsCfgRev{ 1297 makeStoredCfgRev("test-project-a"), 1298 makeStoredCfgRev("@internal"), 1299 } 1300 So(putProjectRealms(ctx, "test-project-a"), ShouldBeNil) 1301 So(putProjectRealms(ctx, "@internal"), ShouldBeNil) 1302 1303 jobs, err := CheckConfigChanges(ctx, permsDB, latest, stored, false, "Updated from update-realms cron job") 1304 So(err, ShouldBeNil) 1305 So(jobs, ShouldBeEmpty) 1306 1307 So(runJobs(jobs), ShouldBeTrue) 1308 So(taskScheduler.Tasks(), ShouldHaveLength, 0) 1309 }) 1310 1311 Convey("add realms for new project", func() { 1312 latest := []*model.RealmsCfgRev{ 1313 makeFetchedCfgRev("@internal"), 1314 makeFetchedCfgRev("test-project-a"), 1315 } 1316 stored := []*model.RealmsCfgRev{ 1317 makeStoredCfgRev("@internal"), 1318 } 1319 So(putProjectRealms(ctx, "@internal"), ShouldBeNil) 1320 1321 jobs, err := CheckConfigChanges(ctx, permsDB, latest, stored, false, "Updated from update-realms cron job") 1322 So(err, ShouldBeNil) 1323 So(jobs, ShouldHaveLength, 1) 1324 1325 So(runJobs(jobs), ShouldBeTrue) 1326 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 1327 }) 1328 1329 Convey("update existing realms.cfg", func() { 1330 latest := []*model.RealmsCfgRev{ 1331 makeFetchedCfgRev("@internal"), 1332 makeFetchedCfgRev("test-project-a"), 1333 } 1334 latest[1].ConfigDigest = "different-digest" 1335 stored := []*model.RealmsCfgRev{ 1336 makeStoredCfgRev("test-project-a"), 1337 makeStoredCfgRev("@internal"), 1338 } 1339 So(putProjectRealms(ctx, "test-project-a"), ShouldBeNil) 1340 So(putProjectRealms(ctx, "@internal"), ShouldBeNil) 1341 1342 jobs, err := CheckConfigChanges(ctx, permsDB, latest, stored, false, "Updated from update-realms cron job") 1343 So(err, ShouldBeNil) 1344 So(jobs, ShouldHaveLength, 1) 1345 1346 So(runJobs(jobs), ShouldBeTrue) 1347 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 1348 }) 1349 1350 Convey("delete project realms if realms.cfg no longer exists", func() { 1351 latest := []*model.RealmsCfgRev{ 1352 makeFetchedCfgRev("@internal"), 1353 } 1354 stored := []*model.RealmsCfgRev{ 1355 makeStoredCfgRev("test-project-a"), 1356 makeStoredCfgRev("@internal"), 1357 } 1358 So(putProjectRealms(ctx, "test-project-a"), ShouldBeNil) 1359 So(putProjectRealms(ctx, "@internal"), ShouldBeNil) 1360 1361 jobs, err := CheckConfigChanges(ctx, permsDB, latest, stored, false, "Updated from update-realms cron job") 1362 So(err, ShouldBeNil) 1363 So(jobs, ShouldHaveLength, 1) 1364 1365 So(runJobs(jobs), ShouldBeTrue) 1366 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 1367 }) 1368 1369 Convey("update if there is a new revision of permissions", func() { 1370 latest := []*model.RealmsCfgRev{ 1371 makeFetchedCfgRev("@internal"), 1372 makeFetchedCfgRev("test-project-a"), 1373 } 1374 stored := []*model.RealmsCfgRev{ 1375 makeStoredCfgRev("test-project-a"), 1376 makeStoredCfgRev("@internal"), 1377 } 1378 stored[1].PermsRev = "permissions.cfg:old" 1379 So(putProjectRealms(ctx, "test-project-a"), ShouldBeNil) 1380 So(putProjectRealms(ctx, "@internal"), ShouldBeNil) 1381 1382 jobs, err := CheckConfigChanges(ctx, permsDB, latest, stored, false, "Updated from update-realms cron job") 1383 So(err, ShouldBeNil) 1384 So(jobs, ShouldHaveLength, 1) 1385 1386 So(runJobs(jobs), ShouldBeTrue) 1387 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 1388 }) 1389 1390 Convey("AuthDB revisions are limited when permissions change", func() { 1391 projectCount := 3 * maxReevaluationRevisions 1392 latest := make([]*model.RealmsCfgRev, projectCount) 1393 stored := make([]*model.RealmsCfgRev, projectCount) 1394 for i := 0; i < projectCount; i++ { 1395 projectID := fmt.Sprintf("test-project-%d", i) 1396 latest[i] = makeFetchedCfgRev(projectID) 1397 stored[i] = makeStoredCfgRev(projectID) 1398 stored[i].PermsRev = "permissions.cfg:old" 1399 So(putProjectRealms(ctx, projectID), ShouldBeNil) 1400 } 1401 1402 jobs, err := CheckConfigChanges(ctx, permsDB, latest, stored, false, "Updated from update-realms cron job") 1403 So(err, ShouldBeNil) 1404 So(jobs, ShouldHaveLength, maxReevaluationRevisions) 1405 1406 So(runJobs(jobs), ShouldBeTrue) 1407 So(taskScheduler.Tasks(), ShouldHaveLength, 2*maxReevaluationRevisions) 1408 }) 1409 }) 1410 1411 } 1412 1413 func sortRevsByID(revs []*model.RealmsCfgRev) { 1414 sort.Slice(revs, func(i, j int) bool { 1415 return revs[i].ProjectID < revs[j].ProjectID 1416 }) 1417 }