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  }