go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/impl/model/changelog_test.go (about)

     1  // Copyright 2021 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 model
    16  
    17  import (
    18  	"context"
    19  	"sort"
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	"github.com/google/go-cmp/cmp/cmpopts"
    25  	"google.golang.org/protobuf/proto"
    26  
    27  	"go.chromium.org/luci/common/clock"
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	"go.chromium.org/luci/common/errors"
    30  	"go.chromium.org/luci/gae/filter/txndefer"
    31  	"go.chromium.org/luci/gae/impl/memory"
    32  	"go.chromium.org/luci/gae/service/datastore"
    33  	"go.chromium.org/luci/server/auth"
    34  	"go.chromium.org/luci/server/auth/authtest"
    35  	"go.chromium.org/luci/server/auth/service/protocol"
    36  	"go.chromium.org/luci/server/tq"
    37  
    38  	"go.chromium.org/luci/auth_service/api/configspb"
    39  	"go.chromium.org/luci/auth_service/impl/info"
    40  
    41  	. "github.com/smartystreets/goconvey/convey"
    42  	. "go.chromium.org/luci/common/testing/assertions"
    43  )
    44  
    45  func testAuthDBGroupChange(ctx context.Context, target string, changeType ChangeType, authDBRev int64) *AuthDBChange {
    46  	change := &AuthDBChange{
    47  		Kind:       "AuthDBChange",
    48  		Parent:     ChangeLogRevisionKey(ctx, authDBRev, false),
    49  		Class:      []string{"AuthDBChange", "AuthDBGroupChange"},
    50  		ChangeType: changeType,
    51  		Target:     target,
    52  		AuthDBRev:  authDBRev,
    53  		Who:        "user:test@example.com",
    54  		When:       time.Date(2020, time.August, 16, 15, 20, 0, 0, time.UTC),
    55  		Comment:    "comment",
    56  		AppVersion: "123-45abc",
    57  	}
    58  
    59  	var err error
    60  	change.ID, err = ChangeID(ctx, change)
    61  	So(err, ShouldBeNil)
    62  	return change
    63  }
    64  
    65  func testAuthDBIPAllowlistChange(ctx context.Context, authDBRev int64) *AuthDBChange {
    66  	change := &AuthDBChange{
    67  		Kind:           "AuthDBChange",
    68  		Parent:         ChangeLogRevisionKey(ctx, authDBRev, false),
    69  		Class:          []string{"AuthDBChange", "AuthDBIPWhitelistChange"},
    70  		ChangeType:     3000,
    71  		Comment:        "comment",
    72  		Description:    "description",
    73  		OldDescription: "",
    74  		Target:         "AuthIPWhitelist$a",
    75  		When:           time.Date(2021, time.December, 12, 1, 0, 0, 0, time.UTC),
    76  		Who:            "user:test@example.com",
    77  		AppVersion:     "123-45abc",
    78  	}
    79  
    80  	var err error
    81  	change.ID, err = ChangeID(ctx, change)
    82  	So(err, ShouldBeNil)
    83  	return change
    84  }
    85  
    86  func testAuthDBIPAllowlistAssignmentChange(ctx context.Context, authDBRev int64) *AuthDBChange {
    87  	change := &AuthDBChange{
    88  		Kind:        "AuthDBChange",
    89  		Parent:      ChangeLogRevisionKey(ctx, authDBRev, false),
    90  		Class:       []string{"AuthDBChange", "AuthDBIPWhitelistAssignmentChange"},
    91  		ChangeType:  5100,
    92  		Comment:     "comment",
    93  		Identity:    "test",
    94  		IPAllowlist: "test",
    95  		Target:      "AuthIPWhitelistAssignments$default$user:test@example.com",
    96  		When:        time.Date(2019, time.December, 12, 1, 0, 0, 0, time.UTC),
    97  		Who:         "user:test@example.com",
    98  		AppVersion:  "123-45abc",
    99  	}
   100  
   101  	var err error
   102  	change.ID, err = ChangeID(ctx, change)
   103  	So(err, ShouldBeNil)
   104  	return change
   105  }
   106  
   107  func testAuthDBConfigChange(ctx context.Context, authDBRev int64) *AuthDBChange {
   108  	change := &AuthDBChange{
   109  		Kind:              "AuthDBChange",
   110  		Parent:            ChangeLogRevisionKey(ctx, authDBRev, false),
   111  		Class:             []string{"AuthDBChange", "AuthDBConfigChange"},
   112  		ChangeType:        7000,
   113  		Comment:           "comment",
   114  		OauthClientID:     "123.test.example.com",
   115  		OauthClientSecret: "aBcD",
   116  		Target:            "AuthGlobalConfig$test",
   117  		When:              time.Date(2019, time.December, 11, 1, 0, 0, 0, time.UTC),
   118  		Who:               "user:test@example.com",
   119  		AppVersion:        "123-45abc",
   120  	}
   121  
   122  	var err error
   123  	change.ID, err = ChangeID(ctx, change)
   124  	So(err, ShouldBeNil)
   125  	return change
   126  }
   127  
   128  func testAuthRealmsGlobalsChange(ctx context.Context, authDBRev int64) *AuthDBChange {
   129  	change := &AuthDBChange{
   130  		Kind:             "AuthDBChange",
   131  		Parent:           ChangeLogRevisionKey(ctx, authDBRev, false),
   132  		Class:            []string{"AuthDBChange", "AuthRealmsGlobalsChange"},
   133  		ChangeType:       9000,
   134  		Comment:          "comment",
   135  		PermissionsAdded: []string{"a.existInRealm"},
   136  		Target:           "AuthRealmsGlobals$globals",
   137  		When:             time.Date(2021, time.January, 11, 1, 0, 0, 0, time.UTC),
   138  		Who:              "user:test@example.com",
   139  		AppVersion:       "123-45abc",
   140  	}
   141  
   142  	var err error
   143  	change.ID, err = ChangeID(ctx, change)
   144  	So(err, ShouldBeNil)
   145  	return change
   146  }
   147  
   148  func testAuthProjectRealmsChange(ctx context.Context, authDBRev int64) *AuthDBChange {
   149  	change := &AuthDBChange{
   150  		Kind:         "AuthDBChange",
   151  		Parent:       ChangeLogRevisionKey(ctx, authDBRev, false),
   152  		Class:        []string{"AuthDBChange", "AuthProjectRealmsChange"},
   153  		ChangeType:   10200,
   154  		Comment:      "comment",
   155  		ConfigRevOld: "",
   156  		ConfigRevNew: "",
   157  		PermsRevOld:  "auth_service_ver:100-00abc",
   158  		PermsRevNew:  "auth_service_ver:123-45abc",
   159  		Target:       "AuthProjectRealms$repo",
   160  		When:         time.Date(2019, time.January, 11, 1, 0, 0, 0, time.UTC),
   161  		Who:          "user:test@example.com",
   162  		AppVersion:   "123-45abc",
   163  	}
   164  
   165  	var err error
   166  	change.ID, err = ChangeID(ctx, change)
   167  	So(err, ShouldBeNil)
   168  	return change
   169  }
   170  
   171  ////////////////////////////////////////////////////////////////////////////////
   172  
   173  func TestGetAllAuthDBChange(t *testing.T) {
   174  	t.Parallel()
   175  	Convey("Testing GetAllAuthDBChange", t, func() {
   176  		ctx := memory.Use(context.Background())
   177  		datastore.GetTestable(ctx).AutoIndex(true)
   178  		datastore.GetTestable(ctx).Consistent(true)
   179  
   180  		So(datastore.Put(ctx,
   181  			testAuthDBGroupChange(ctx, "AuthGroup$groupA", ChangeGroupCreated, 1120),
   182  			testAuthDBGroupChange(ctx, "AuthGroup$groupB", ChangeGroupMembersAdded, 1120),
   183  			testAuthDBGroupChange(ctx, "AuthGroup$groupB", ChangeGroupMembersAdded, 1136),
   184  			testAuthDBGroupChange(ctx, "AuthGroup$groupC", ChangeGroupMembersAdded, 1135),
   185  			testAuthDBGroupChange(ctx, "AuthGroup$groupC", ChangeGroupOwnersChanged, 1110),
   186  			testAuthDBIPAllowlistChange(ctx, 1120),
   187  			testAuthDBIPAllowlistAssignmentChange(ctx, 1121),
   188  			testAuthDBConfigChange(ctx, 1115),
   189  			testAuthRealmsGlobalsChange(ctx, 1120),
   190  			testAuthProjectRealmsChange(ctx, 1115),
   191  		), ShouldBeNil)
   192  
   193  		Convey("Sort by key", func() {
   194  			changes, pageToken, err := GetAllAuthDBChange(ctx, "", 0, 5, "")
   195  			So(err, ShouldBeNil)
   196  			So(pageToken, ShouldNotBeEmpty)
   197  			So(changes, ShouldResemble, []*AuthDBChange{
   198  				testAuthDBGroupChange(ctx, "AuthGroup$groupB", ChangeGroupMembersAdded, 1136),
   199  				testAuthDBGroupChange(ctx, "AuthGroup$groupC", ChangeGroupMembersAdded, 1135),
   200  				testAuthDBIPAllowlistAssignmentChange(ctx, 1121),
   201  				testAuthRealmsGlobalsChange(ctx, 1120),
   202  				testAuthDBIPAllowlistChange(ctx, 1120),
   203  			})
   204  
   205  			changes, _, err = GetAllAuthDBChange(ctx, "", 0, 5, pageToken)
   206  			So(err, ShouldBeNil)
   207  			So(changes, ShouldResemble, []*AuthDBChange{
   208  				testAuthDBGroupChange(ctx, "AuthGroup$groupB", ChangeGroupMembersAdded, 1120),
   209  				testAuthDBGroupChange(ctx, "AuthGroup$groupA", ChangeGroupCreated, 1120),
   210  				testAuthProjectRealmsChange(ctx, 1115),
   211  				testAuthDBConfigChange(ctx, 1115),
   212  				testAuthDBGroupChange(ctx, "AuthGroup$groupC", ChangeGroupOwnersChanged, 1110),
   213  			})
   214  		})
   215  		Convey("Filter by target", func() {
   216  			changes, _, err := GetAllAuthDBChange(ctx, "AuthGroup$groupB", 0, 10, "")
   217  			So(err, ShouldBeNil)
   218  			So(changes, ShouldResemble, []*AuthDBChange{
   219  				testAuthDBGroupChange(ctx, "AuthGroup$groupB", ChangeGroupMembersAdded, 1136),
   220  				testAuthDBGroupChange(ctx, "AuthGroup$groupB", ChangeGroupMembersAdded, 1120),
   221  			})
   222  		})
   223  		Convey("Filter by authDBRev", func() {
   224  			changes, _, err := GetAllAuthDBChange(ctx, "", 1136, 10, "")
   225  			So(err, ShouldBeNil)
   226  			So(changes, ShouldResemble, []*AuthDBChange{
   227  				testAuthDBGroupChange(ctx, "AuthGroup$groupB", ChangeGroupMembersAdded, 1136),
   228  			})
   229  		})
   230  		Convey("Return error when target is invalid", func() {
   231  			_, _, err := GetAllAuthDBChange(ctx, "groupname", 0, 10, "")
   232  			So(err, ShouldErrLike, "Invalid target groupname")
   233  		})
   234  	})
   235  }
   236  
   237  func TestGenerateChanges(t *testing.T) {
   238  	t.Parallel()
   239  
   240  	Convey("GenerateChanges", t, func() {
   241  		ctx := auth.WithState(memory.Use(context.Background()), &authtest.FakeState{
   242  			Identity:       "user:someone@example.com",
   243  			IdentityGroups: []string{AdminGroup},
   244  		})
   245  		ctx = info.SetImageVersion(ctx, "test-version")
   246  		ctx = clock.Set(ctx, testclock.New(testCreatedTS))
   247  		ctx, taskScheduler := tq.TestingContext(txndefer.FilterRDS(ctx), nil)
   248  
   249  		So(datastore.Put(ctx, testAuthGroup(ctx, AdminGroup)), ShouldBeNil)
   250  
   251  		//////////////////////////////////////////////////////////
   252  		// Helper functions
   253  		getChanges := func(ctx context.Context, authDBRev int64, dryRun bool) []*AuthDBChange {
   254  			ancestor := constructLogRevisionKey(ctx, authDBRev, dryRun)
   255  			query := datastore.NewQuery(entityKind("AuthDBChange", dryRun)).Ancestor(ancestor)
   256  			changes := []*AuthDBChange{}
   257  			err := datastore.Run(ctx, query, func(change *AuthDBChange) {
   258  				changes = append(changes, change)
   259  			})
   260  			So(err, ShouldBeNil)
   261  			return changes
   262  		}
   263  
   264  		// The fields in AuthDBChange to ignore when comparing results;
   265  		// these fields are not signficant to the test cases below.
   266  		ignoredAuthDBChangeFields := cmpopts.IgnoreFields(AuthDBChange{},
   267  			"Kind", "ID", "Parent", "Class", "Target", "Who", "When", "Comment", "AppVersion")
   268  
   269  		validateChanges := func(ctx context.Context, msg string, authDBRev int64, actualChanges []*AuthDBChange, expectedChanges []*AuthDBChange) {
   270  			changeCount := len(expectedChanges)
   271  			SoMsg(msg, actualChanges, ShouldHaveLength, changeCount)
   272  
   273  			// Check each actual and exxpected changes are similar.
   274  			for i := 0; i < changeCount; i++ {
   275  				// Set the expected AuthDB revision to the given value.
   276  				expectedChanges[i].AuthDBRev = authDBRev
   277  
   278  				diff := cmp.Diff(actualChanges[i], expectedChanges[i], ignoredAuthDBChangeFields)
   279  				if diff != "" {
   280  					t.Errorf("%s - difference at index %d: %s", msg, i, diff)
   281  				}
   282  			}
   283  
   284  			// Check AuthDBChange records are in datastore.
   285  			sort.Slice(actualChanges, func(i, j int) bool {
   286  				return actualChanges[i].ChangeType < actualChanges[j].ChangeType
   287  			})
   288  			SoMsg(msg, getChanges(ctx, authDBRev, false), ShouldResemble, actualChanges)
   289  		}
   290  		//////////////////////////////////////////////////////////
   291  
   292  		Convey("AuthGroup changes", func() {
   293  			Convey("AuthGroup Created/Deleted", func() {
   294  				ag1 := makeAuthGroup(ctx, "group-1")
   295  				_, err := CreateAuthGroup(ctx, ag1, false, "Go pRPC API", false)
   296  				So(err, ShouldBeNil)
   297  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   298  				actualChanges, err := generateChanges(ctx, 1, false)
   299  				So(err, ShouldBeNil)
   300  				// Check that common fields were set as expected; these fields
   301  				// are ignored for most other test cases.
   302  				So(actualChanges, ShouldResembleProto, []*AuthDBChange{{
   303  					ID:         "AuthGroup$group-1!1000",
   304  					Class:      []string{"AuthDBChange", "AuthDBGroupChange"},
   305  					ChangeType: ChangeGroupCreated,
   306  					Target:     "AuthGroup$group-1",
   307  					Kind:       "AuthDBChange",
   308  					Parent:     ChangeLogRevisionKey(ctx, 1, false),
   309  					AuthDBRev:  1,
   310  					Who:        "user:someone@example.com",
   311  					When:       testCreatedTS,
   312  					Comment:    "Go pRPC API",
   313  					AppVersion: "test-version",
   314  					Owners:     AdminGroup,
   315  				}})
   316  				validateChanges(ctx, "create group", 1, actualChanges, []*AuthDBChange{{
   317  					ChangeType: ChangeGroupCreated,
   318  					Owners:     AdminGroup,
   319  				}})
   320  
   321  				So(DeleteAuthGroup(ctx, ag1.ID, "", false, "Go pRPC API", false), ShouldBeNil)
   322  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   323  				actualChanges, err = generateChanges(ctx, 2, false)
   324  				So(err, ShouldBeNil)
   325  				validateChanges(ctx, "delete group", 2, actualChanges, []*AuthDBChange{{
   326  					ChangeType: ChangeGroupDeleted,
   327  					OldOwners:  AdminGroup,
   328  				}})
   329  
   330  				// Check calling generateChanges for an already-processed
   331  				// AuthDB revision does not make duplicate changes.
   332  				repeated, err := generateChanges(ctx, 2, false)
   333  				So(err, ShouldBeNil)
   334  				So(repeated, ShouldBeNil)
   335  				// Check the changelog for the revision actually exists.
   336  				expectedStoredChanges := []*AuthDBChange(actualChanges)
   337  				sort.Slice(expectedStoredChanges, func(i, j int) bool {
   338  					return expectedStoredChanges[i].ChangeType < expectedStoredChanges[j].ChangeType
   339  				})
   340  				So(getChanges(ctx, 2, false), ShouldResemble, expectedStoredChanges)
   341  			})
   342  
   343  			Convey("AuthGroup Owners / Description changed", func() {
   344  				og := makeAuthGroup(ctx, "owning-group")
   345  				So(datastore.Put(ctx, og), ShouldBeNil)
   346  
   347  				ag1 := makeAuthGroup(ctx, "group-1")
   348  				_, err := CreateAuthGroup(ctx, ag1, false, "Go pRPC API", false)
   349  				So(err, ShouldBeNil)
   350  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   351  				actualChanges, err := generateChanges(ctx, 1, false)
   352  				So(err, ShouldBeNil)
   353  				validateChanges(ctx, "create group no owners", 1, actualChanges, []*AuthDBChange{{
   354  					ChangeType: ChangeGroupCreated,
   355  					Owners:     AdminGroup,
   356  				}})
   357  
   358  				ag1.Owners = og.ID
   359  				ag1.Description = "test-desc"
   360  				_, err = UpdateAuthGroup(ctx, ag1, nil, "", false, "Go pRPC API", false)
   361  				So(err, ShouldBeNil)
   362  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   363  				actualChanges, err = generateChanges(ctx, 2, false)
   364  				So(err, ShouldBeNil)
   365  				validateChanges(ctx, "update group owners & desc", 2, actualChanges, []*AuthDBChange{{
   366  					ChangeType:  ChangeGroupDescriptionChanged,
   367  					Description: "test-desc",
   368  				}, {
   369  					ChangeType: ChangeGroupOwnersChanged,
   370  					Owners:     "owning-group",
   371  					OldOwners:  AdminGroup,
   372  				}})
   373  
   374  				ag1.Description = "new-desc"
   375  				_, err = UpdateAuthGroup(ctx, ag1, nil, "", false, "Go pRPC API", false)
   376  				So(err, ShouldBeNil)
   377  				So(taskScheduler.Tasks(), ShouldHaveLength, 6)
   378  				actualChanges, err = generateChanges(ctx, 3, false)
   379  				So(err, ShouldBeNil)
   380  				validateChanges(ctx, "update group desc", 3, actualChanges, []*AuthDBChange{{
   381  					ChangeType:     ChangeGroupDescriptionChanged,
   382  					Description:    "new-desc",
   383  					OldDescription: "test-desc",
   384  				}})
   385  			})
   386  
   387  			Convey("AuthGroup add/remove Members", func() {
   388  				// Add members in Create
   389  				ag1 := makeAuthGroup(ctx, "group-1")
   390  				ag1.Members = []string{"user:someone@example.com"}
   391  				_, err := CreateAuthGroup(ctx, ag1, false, "Go pRPC API", false)
   392  				So(err, ShouldBeNil)
   393  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   394  				actualChanges, err := generateChanges(ctx, 1, false)
   395  				So(err, ShouldBeNil)
   396  				validateChanges(ctx, "create group +mems", 1, actualChanges, []*AuthDBChange{{
   397  					ChangeType: ChangeGroupCreated,
   398  					Owners:     AdminGroup,
   399  				}, {
   400  					ChangeType: ChangeGroupMembersAdded,
   401  					Members:    []string{"user:someone@example.com"},
   402  				}})
   403  
   404  				// Add members to already existing group
   405  				ag1.Members = append(ag1.Members, "user:another-one@example.com")
   406  				_, err = UpdateAuthGroup(ctx, ag1, nil, "", false, "Go pRPC API", false)
   407  				So(err, ShouldBeNil)
   408  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   409  				actualChanges, err = generateChanges(ctx, 2, false)
   410  				So(err, ShouldBeNil)
   411  				validateChanges(ctx, "update group +mems", 2, actualChanges, []*AuthDBChange{{
   412  					ChangeType: ChangeGroupMembersAdded,
   413  					Members:    []string{"user:another-one@example.com"},
   414  				}})
   415  
   416  				// Remove members from existing group
   417  				ag1.Members = []string{"user:someone@example.com"}
   418  				_, err = UpdateAuthGroup(ctx, ag1, nil, "", false, "Go pRPC API", false)
   419  				So(err, ShouldBeNil)
   420  				So(taskScheduler.Tasks(), ShouldHaveLength, 6)
   421  				actualChanges, err = generateChanges(ctx, 3, false)
   422  				So(err, ShouldBeNil)
   423  				validateChanges(ctx, "update group -mems", 3, actualChanges, []*AuthDBChange{{
   424  					ChangeType: ChangeGroupMembersRemoved,
   425  					Members:    []string{"user:another-one@example.com"},
   426  				}})
   427  
   428  				// Remove members when deleting group
   429  				So(DeleteAuthGroup(ctx, ag1.ID, "", false, "Go pRPC API", false), ShouldBeNil)
   430  				So(taskScheduler.Tasks(), ShouldHaveLength, 8)
   431  				actualChanges, err = generateChanges(ctx, 4, false)
   432  				So(err, ShouldBeNil)
   433  				validateChanges(ctx, "delete group -mems", 4, actualChanges, []*AuthDBChange{{
   434  					ChangeType: ChangeGroupMembersRemoved,
   435  					Members:    []string{"user:someone@example.com"},
   436  				}, {
   437  					ChangeType: ChangeGroupDeleted,
   438  					OldOwners:  AdminGroup,
   439  				}})
   440  			})
   441  
   442  			Convey("AuthGroup add/remove globs", func() {
   443  				// Add globs in create
   444  				ag1 := makeAuthGroup(ctx, "group-1")
   445  				ag1.Globs = []string{"user:*@example.com"}
   446  				_, err := CreateAuthGroup(ctx, ag1, false, "Go pRPC API", false)
   447  				So(err, ShouldBeNil)
   448  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   449  				actualChanges, err := generateChanges(ctx, 1, false)
   450  				So(err, ShouldBeNil)
   451  				validateChanges(ctx, "create group +globs", 1, actualChanges, []*AuthDBChange{{
   452  					ChangeType: ChangeGroupCreated,
   453  					Owners:     AdminGroup,
   454  				}, {
   455  					ChangeType: ChangeGroupGlobsAdded,
   456  					Globs:      []string{"user:*@example.com"},
   457  				}})
   458  
   459  				// Add globs to already existing group
   460  				ag1.Globs = append(ag1.Globs, "user:test-*@test.com")
   461  				_, err = UpdateAuthGroup(ctx, ag1, nil, "", false, "Go pRPC API", false)
   462  				So(err, ShouldBeNil)
   463  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   464  				actualChanges, err = generateChanges(ctx, 2, false)
   465  				So(err, ShouldBeNil)
   466  				validateChanges(ctx, "update group +globs", 2, actualChanges, []*AuthDBChange{{
   467  					ChangeType: ChangeGroupGlobsAdded,
   468  					Globs:      []string{"user:test-*@test.com"},
   469  				}})
   470  
   471  				ag1.Globs = []string{"user:test-*@test.com"}
   472  				_, err = UpdateAuthGroup(ctx, ag1, nil, "", false, "Go pRPC API", false)
   473  				So(err, ShouldBeNil)
   474  				So(taskScheduler.Tasks(), ShouldHaveLength, 6)
   475  				So(err, ShouldBeNil)
   476  				actualChanges, err = generateChanges(ctx, 3, false)
   477  				So(err, ShouldBeNil)
   478  				validateChanges(ctx, "update group -globs", 3, actualChanges, []*AuthDBChange{{
   479  					ChangeType: ChangeGroupGlobsRemoved,
   480  					Globs:      []string{"user:*@example.com"},
   481  				}})
   482  
   483  				So(DeleteAuthGroup(ctx, ag1.ID, "", false, "Go pRPC API", false), ShouldBeNil)
   484  				So(taskScheduler.Tasks(), ShouldHaveLength, 8)
   485  				actualChanges, err = generateChanges(ctx, 4, false)
   486  				So(err, ShouldBeNil)
   487  				validateChanges(ctx, "delete group -globs", 4, actualChanges, []*AuthDBChange{{
   488  					ChangeType: ChangeGroupGlobsRemoved,
   489  					Globs:      []string{"user:test-*@test.com"},
   490  				}, {
   491  					ChangeType: ChangeGroupDeleted,
   492  					OldOwners:  AdminGroup,
   493  				}})
   494  			})
   495  
   496  			Convey("AuthGroup add/remove nested", func() {
   497  				ag2 := makeAuthGroup(ctx, "group-2")
   498  				ag3 := makeAuthGroup(ctx, "group-3")
   499  				So(datastore.Put(ctx, ag2, ag3), ShouldBeNil)
   500  
   501  				// Add globs in create
   502  				ag1 := makeAuthGroup(ctx, "group-1")
   503  				ag1.Nested = []string{ag2.ID}
   504  				_, err := CreateAuthGroup(ctx, ag1, false, "Go pRPC API", false)
   505  				So(err, ShouldBeNil)
   506  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   507  				actualChanges, err := generateChanges(ctx, 1, false)
   508  				So(err, ShouldBeNil)
   509  				validateChanges(ctx, "create group +nested", 1, actualChanges, []*AuthDBChange{{
   510  					ChangeType: ChangeGroupCreated,
   511  					Owners:     AdminGroup,
   512  				}, {
   513  					ChangeType: ChangeGroupNestedAdded,
   514  					Nested:     []string{"group-2"},
   515  				}})
   516  
   517  				// Add globs to already existing group
   518  				ag1.Nested = append(ag1.Nested, ag3.ID)
   519  				_, err = UpdateAuthGroup(ctx, ag1, nil, "", false, "Go pRPC API", false)
   520  				So(err, ShouldBeNil)
   521  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   522  				actualChanges, err = generateChanges(ctx, 2, false)
   523  				So(err, ShouldBeNil)
   524  				validateChanges(ctx, "update group +nested", 2, actualChanges, []*AuthDBChange{{
   525  					ChangeType: ChangeGroupNestedAdded,
   526  					Nested:     []string{"group-3"},
   527  				}})
   528  
   529  				ag1.Nested = []string{"group-2"}
   530  				_, err = UpdateAuthGroup(ctx, ag1, nil, "", false, "Go pRPC API", false)
   531  				So(err, ShouldBeNil)
   532  				So(taskScheduler.Tasks(), ShouldHaveLength, 6)
   533  				So(err, ShouldBeNil)
   534  				actualChanges, err = generateChanges(ctx, 3, false)
   535  				So(err, ShouldBeNil)
   536  				validateChanges(ctx, "update group -nested", 3, actualChanges, []*AuthDBChange{{
   537  					ChangeType: ChangeGroupNestedRemoved,
   538  					Nested:     []string{"group-3"},
   539  				}})
   540  
   541  				So(DeleteAuthGroup(ctx, ag1.ID, "", false, "Go pRPC API", false), ShouldBeNil)
   542  				So(taskScheduler.Tasks(), ShouldHaveLength, 8)
   543  				actualChanges, err = generateChanges(ctx, 4, false)
   544  				So(err, ShouldBeNil)
   545  				validateChanges(ctx, "delete group -nested", 4, actualChanges, []*AuthDBChange{{
   546  					ChangeType: ChangeGroupNestedRemoved,
   547  					Nested:     []string{"group-2"},
   548  				}, {
   549  					ChangeType: ChangeGroupDeleted,
   550  					OldOwners:  AdminGroup,
   551  				}})
   552  			})
   553  		})
   554  
   555  		Convey("AuthIPAllowlist changes", func() {
   556  			Convey("AuthIPAllowlist Created/Deleted +/- subnets", func() {
   557  				// Creation with no subnet
   558  				baseSubnetMap := make(map[string][]string)
   559  				baseSubnetMap["test-allowlist-1"] = []string{}
   560  				So(UpdateAllowlistEntities(ctx, baseSubnetMap, false, "Go pRPC API"), ShouldBeNil)
   561  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   562  				actualChanges, err := generateChanges(ctx, 1, false)
   563  				So(err, ShouldBeNil)
   564  				validateChanges(ctx, "create allowlist", 1, actualChanges, []*AuthDBChange{{
   565  					ChangeType:  ChangeIPALCreated,
   566  					Description: "Imported from ip_allowlist.cfg",
   567  				}})
   568  
   569  				// Deletion with no subnet
   570  				baseSubnetMap = map[string][]string{}
   571  				So(UpdateAllowlistEntities(ctx, baseSubnetMap, false, "Go pRPC API"), ShouldBeNil)
   572  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   573  				actualChanges, err = generateChanges(ctx, 2, false)
   574  				So(err, ShouldBeNil)
   575  				validateChanges(ctx, "delete allowlist", 2, actualChanges, []*AuthDBChange{{
   576  					ChangeType:     ChangeIPALDeleted,
   577  					OldDescription: "Imported from ip_allowlist.cfg",
   578  				}})
   579  
   580  				// Creation with subnets
   581  				baseSubnetMap["test-allowlist-1"] = []string{"123.4.5.6"}
   582  				So(UpdateAllowlistEntities(ctx, baseSubnetMap, false, "Go pRPC API"), ShouldBeNil)
   583  				So(taskScheduler.Tasks(), ShouldHaveLength, 6)
   584  				actualChanges, err = generateChanges(ctx, 3, false)
   585  				So(err, ShouldBeNil)
   586  				validateChanges(ctx, "create allowlist w/ subnet", 3, actualChanges, []*AuthDBChange{{
   587  					ChangeType:  ChangeIPALCreated,
   588  					Description: "Imported from ip_allowlist.cfg",
   589  				}, {
   590  					ChangeType: ChangeIPALSubnetsAdded,
   591  					Subnets:    []string{"123.4.5.6"},
   592  				}})
   593  
   594  				// Add subnet
   595  				baseSubnetMap["test-allowlist-1"] = append(baseSubnetMap["test-allowlist-1"], "567.8.9.10")
   596  				So(UpdateAllowlistEntities(ctx, baseSubnetMap, false, "Go pRPC API"), ShouldBeNil)
   597  				So(taskScheduler.Tasks(), ShouldHaveLength, 8)
   598  				actualChanges, err = generateChanges(ctx, 4, false)
   599  				So(err, ShouldBeNil)
   600  				validateChanges(ctx, "add subnet", 4, actualChanges, []*AuthDBChange{{
   601  					ChangeType: ChangeIPALSubnetsAdded,
   602  					Subnets:    []string{"567.8.9.10"},
   603  				}})
   604  
   605  				// Remove subnet
   606  				baseSubnetMap["test-allowlist-1"] = baseSubnetMap["test-allowlist-1"][1:]
   607  				So(UpdateAllowlistEntities(ctx, baseSubnetMap, false, "Go pRPC API"), ShouldBeNil)
   608  				So(taskScheduler.Tasks(), ShouldHaveLength, 10)
   609  				actualChanges, err = generateChanges(ctx, 5, false)
   610  				So(err, ShouldBeNil)
   611  				validateChanges(ctx, "remove subnet", 5, actualChanges, []*AuthDBChange{{
   612  					ChangeType: ChangeIPALSubnetsRemoved,
   613  					Subnets:    []string{"123.4.5.6"},
   614  				}})
   615  
   616  				// Delete allowlist with subnet
   617  				baseSubnetMap = map[string][]string{}
   618  				So(UpdateAllowlistEntities(ctx, baseSubnetMap, false, "Go pRPC API"), ShouldBeNil)
   619  				So(taskScheduler.Tasks(), ShouldHaveLength, 12)
   620  				actualChanges, err = generateChanges(ctx, 6, false)
   621  				So(err, ShouldBeNil)
   622  				validateChanges(ctx, "delete allowlist w/ subnet", 6, actualChanges, []*AuthDBChange{{
   623  					ChangeType:     ChangeIPALDeleted,
   624  					OldDescription: "Imported from ip_allowlist.cfg",
   625  				}, {
   626  					ChangeType: ChangeIPALSubnetsRemoved,
   627  					Subnets:    []string{"567.8.9.10"},
   628  				}})
   629  			})
   630  
   631  			Convey("AuthIPAllowlist description changed", func() {
   632  				baseSubnetMap := make(map[string][]string)
   633  				baseSubnetMap["test-allowlist-1"] = []string{}
   634  				So(UpdateAllowlistEntities(ctx, baseSubnetMap, false, "Go pRPC API"), ShouldBeNil)
   635  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   636  				_, err := generateChanges(ctx, 1, false)
   637  				So(err, ShouldBeNil)
   638  
   639  				al, err := GetAuthIPAllowlist(ctx, "test-allowlist-1")
   640  				So(err, ShouldBeNil)
   641  				So(runAuthDBChange(ctx, "test for AuthIPAllowlist changelog", func(ctx context.Context, cae commitAuthEntity) error {
   642  					al.Description = "new-desc"
   643  					return cae(al, clock.Now(ctx).UTC(), auth.CurrentIdentity(ctx), false)
   644  				}), ShouldBeNil)
   645  				actualChanges, err := generateChanges(ctx, 2, false)
   646  				So(err, ShouldBeNil)
   647  				validateChanges(ctx, "change description", 2, actualChanges, []*AuthDBChange{{
   648  					ChangeType:     ChangeIPALDescriptionChanged,
   649  					Description:    "new-desc",
   650  					OldDescription: "Imported from ip_allowlist.cfg",
   651  				}})
   652  			})
   653  		})
   654  
   655  		Convey("AuthGlobalConfig changes", func() {
   656  			Convey("AuthGlobalConfig ClientID/ClientSecret mismatch", func() {
   657  				baseCfg := &configspb.OAuthConfig{
   658  					PrimaryClientId:     "test-client-id",
   659  					PrimaryClientSecret: "test-client-secret",
   660  				}
   661  
   662  				// Old doesn't exist yet
   663  				So(UpdateAuthGlobalConfig(ctx, baseCfg, nil, false, "Go pRPC API"), ShouldBeNil)
   664  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   665  				actualChanges, err := generateChanges(ctx, 1, false)
   666  				So(err, ShouldBeNil)
   667  				validateChanges(ctx, "update config with no old config present", 1, actualChanges, []*AuthDBChange{{
   668  					ChangeType:        ChangeConfOauthClientChanged,
   669  					OauthClientID:     "test-client-id",
   670  					OauthClientSecret: "test-client-secret",
   671  				}})
   672  
   673  				newCfg := &configspb.OAuthConfig{
   674  					PrimaryClientId: "diff-client-id",
   675  				}
   676  				So(UpdateAuthGlobalConfig(ctx, newCfg, nil, false, "Go pRPC API"), ShouldBeNil)
   677  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   678  				actualChanges, err = generateChanges(ctx, 2, false)
   679  				So(err, ShouldBeNil)
   680  				validateChanges(ctx, "update config with client id changed", 2, actualChanges, []*AuthDBChange{{
   681  					ChangeType:    ChangeConfOauthClientChanged,
   682  					OauthClientID: "diff-client-id",
   683  				}})
   684  			})
   685  
   686  			Convey("AuthGlobalConfig additional +/- ClientID's", func() {
   687  				baseCfg := &configspb.OAuthConfig{
   688  					ClientIds: []string{"test.example.com"},
   689  				}
   690  
   691  				So(UpdateAuthGlobalConfig(ctx, baseCfg, nil, false, "Go pRPC API"), ShouldBeNil)
   692  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   693  				actualChanges, err := generateChanges(ctx, 1, false)
   694  				So(err, ShouldBeNil)
   695  				validateChanges(ctx, "update config with client ids, old config not present", 1, actualChanges, []*AuthDBChange{{
   696  					ChangeType:               ChangeConfClientIDsAdded,
   697  					OauthAdditionalClientIDs: []string{"test.example.com"},
   698  				}})
   699  
   700  				newCfg := &configspb.OAuthConfig{
   701  					ClientIds: []string{"not-test.example.com"},
   702  				}
   703  				So(UpdateAuthGlobalConfig(ctx, newCfg, nil, false, "Go pRPC API"), ShouldBeNil)
   704  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   705  				actualChanges, err = generateChanges(ctx, 2, false)
   706  				So(err, ShouldBeNil)
   707  				validateChanges(ctx, "update config with client id added and client id removed, old config is present",
   708  					2, actualChanges, []*AuthDBChange{{
   709  						ChangeType:               ChangeConfClientIDsAdded,
   710  						OauthAdditionalClientIDs: []string{"not-test.example.com"},
   711  					}, {
   712  						ChangeType:               ChangeConfClientIDsRemoved,
   713  						OauthAdditionalClientIDs: []string{"test.example.com"},
   714  					}})
   715  			})
   716  
   717  			Convey("AuthGlobalConfig TokenServerURL change", func() {
   718  				baseCfg := &configspb.OAuthConfig{
   719  					TokenServerUrl: "test-token-server-url.example.com",
   720  				}
   721  
   722  				So(UpdateAuthGlobalConfig(ctx, baseCfg, nil, false, "Go pRPC API"), ShouldBeNil)
   723  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   724  				actualChanges, err := generateChanges(ctx, 1, false)
   725  				So(err, ShouldBeNil)
   726  				validateChanges(ctx, "update config with token server url, old config not present", 1, actualChanges, []*AuthDBChange{{
   727  					ChangeType:        ChangeConfTokenServerURLChanged,
   728  					TokenServerURLNew: "test-token-server-url.example.com",
   729  				}})
   730  			})
   731  
   732  			Convey("AuthGlobalConfig Security Config change", func() {
   733  				baseCfg := &configspb.OAuthConfig{}
   734  				secCfg := &protocol.SecurityConfig{
   735  					InternalServiceRegexp: []string{"abc"},
   736  				}
   737  				So(UpdateAuthGlobalConfig(ctx, baseCfg, secCfg, false, "Go pRPC API"), ShouldBeNil)
   738  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   739  				actualChanges, err := generateChanges(ctx, 1, false)
   740  				So(err, ShouldBeNil)
   741  				expectedNewConfig, _ := proto.Marshal(secCfg)
   742  				validateChanges(ctx, "update config with security config, old config not present", 1, actualChanges, []*AuthDBChange{{
   743  					ChangeType:        ChangeConfSecurityConfigChanged,
   744  					SecurityConfigNew: expectedNewConfig,
   745  				}})
   746  
   747  				secCfg = &protocol.SecurityConfig{
   748  					InternalServiceRegexp: []string{"def"},
   749  				}
   750  				expectedOldConfig := expectedNewConfig
   751  				expectedNewConfig, _ = proto.Marshal(secCfg)
   752  				So(UpdateAuthGlobalConfig(ctx, baseCfg, secCfg, false, "Go pRPC API"), ShouldBeNil)
   753  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
   754  				actualChanges, err = generateChanges(ctx, 2, false)
   755  				So(err, ShouldBeNil)
   756  				validateChanges(ctx, "update config with security config, old config present", 2, actualChanges, []*AuthDBChange{{
   757  					ChangeType:        ChangeConfSecurityConfigChanged,
   758  					SecurityConfigNew: expectedNewConfig,
   759  					SecurityConfigOld: expectedOldConfig,
   760  				}})
   761  			})
   762  
   763  			Convey("AuthGlobalConfig all changes at once", func() {
   764  				baseCfg := &configspb.OAuthConfig{
   765  					PrimaryClientId:     "test-client-id",
   766  					PrimaryClientSecret: "test-client-secret",
   767  					ClientIds:           []string{"a", "b", "c"},
   768  					TokenServerUrl:      "token-server.example.com",
   769  				}
   770  				secCfg := &protocol.SecurityConfig{
   771  					InternalServiceRegexp: []string{"test"},
   772  				}
   773  				expectedNewConfig, _ := proto.Marshal(secCfg)
   774  				So(UpdateAuthGlobalConfig(ctx, baseCfg, secCfg, false, "Go pRPC API"), ShouldBeNil)
   775  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
   776  				actualChanges, err := generateChanges(ctx, 1, false)
   777  				So(err, ShouldBeNil)
   778  				validateChanges(ctx, "all changes at once, old config not present", 1, actualChanges, []*AuthDBChange{{
   779  					ChangeType:        ChangeConfOauthClientChanged,
   780  					OauthClientID:     "test-client-id",
   781  					OauthClientSecret: "test-client-secret",
   782  				}, {
   783  					ChangeType:               ChangeConfClientIDsAdded,
   784  					OauthAdditionalClientIDs: []string{"a", "b", "c"},
   785  				}, {
   786  					ChangeType:        ChangeConfTokenServerURLChanged,
   787  					TokenServerURLNew: "token-server.example.com",
   788  				}, {
   789  					ChangeType:        ChangeConfSecurityConfigChanged,
   790  					SecurityConfigNew: expectedNewConfig,
   791  				}})
   792  			})
   793  		})
   794  
   795  		Convey("AuthProjectRealms changes", func() {
   796  			proj1Realms := &protocol.Realms{
   797  				Permissions: makeTestPermissions("luci.dev.p2", "luci.dev.z", "luci.dev.p1"),
   798  				Realms: []*protocol.Realm{
   799  					{
   800  						Name: "proj1:@root",
   801  						Bindings: []*protocol.Binding{
   802  							{
   803  								// Permissions p2, z, p1.
   804  								Permissions: []uint32{0, 1, 2},
   805  								Principals:  []string{"group:gr1"},
   806  							},
   807  						},
   808  					},
   809  				},
   810  			}
   811  
   812  			// Set up existing project realms.
   813  			expandedRealms := []*ExpandedRealms{
   814  				{
   815  					CfgRev: &RealmsCfgRev{
   816  						ProjectID:    "proj1",
   817  						ConfigRev:    "10001",
   818  						ConfigDigest: "test config digest",
   819  					},
   820  					Realms: proj1Realms,
   821  				},
   822  			}
   823  			err := UpdateAuthProjectRealms(ctx, expandedRealms, "permissions.cfg:abc", false, "Go pRPC API")
   824  			So(err, ShouldBeNil)
   825  
   826  			Convey("project realms created", func() {
   827  				expandedRealms := []*ExpandedRealms{
   828  					{
   829  						CfgRev: &RealmsCfgRev{
   830  							ProjectID:    "proj2",
   831  							ConfigRev:    "1",
   832  							ConfigDigest: "test config digest",
   833  						},
   834  					},
   835  				}
   836  
   837  				err := UpdateAuthProjectRealms(ctx, expandedRealms, "permissions.cfg:abc", false, "Go pRPC API")
   838  				So(err, ShouldBeNil)
   839  
   840  				actualChanges, err := generateChanges(ctx, 2, false)
   841  				So(err, ShouldBeNil)
   842  				validateChanges(ctx, "project realms created", 2, actualChanges, []*AuthDBChange{{
   843  					ChangeType:   ChangeProjectRealmsCreated,
   844  					ConfigRevNew: "1",
   845  					PermsRevNew:  "permissions.cfg:abc",
   846  				}})
   847  			})
   848  
   849  			Convey("project realms deleted", func() {
   850  				err = DeleteAuthProjectRealms(ctx, "proj1", false, "Go pRPC API")
   851  				So(err, ShouldBeNil)
   852  
   853  				actualChanges, err := generateChanges(ctx, 2, false)
   854  				So(err, ShouldBeNil)
   855  				validateChanges(ctx, "project realms removed", 2, actualChanges, []*AuthDBChange{{
   856  					ChangeType:   ChangeProjectRealmsRemoved,
   857  					ConfigRevOld: "10001",
   858  					PermsRevOld:  "permissions.cfg:abc",
   859  				}})
   860  			})
   861  
   862  			Convey("project config superficially changed", func() {
   863  				// New config revision, but the resulting realms are identical.
   864  				updatedExpandedRealms := []*ExpandedRealms{
   865  					{
   866  						CfgRev: &RealmsCfgRev{
   867  							ProjectID:    "proj1",
   868  							ConfigRev:    "10002",
   869  							ConfigDigest: "test config digest",
   870  						},
   871  						Realms: proj1Realms,
   872  					},
   873  				}
   874  				err = UpdateAuthProjectRealms(ctx, updatedExpandedRealms, "permissions.cfg:abc", false, "Go pRPC API")
   875  				So(err, ShouldBeNil)
   876  
   877  				actualChanges, err := generateChanges(ctx, 2, false)
   878  				So(err, ShouldBeNil)
   879  				So(actualChanges, ShouldBeEmpty)
   880  			})
   881  
   882  			Convey("project config revision changed", func() {
   883  				updatedExpandedRealms := []*ExpandedRealms{
   884  					{
   885  						CfgRev: &RealmsCfgRev{
   886  							ProjectID:    "proj1",
   887  							ConfigRev:    "10002",
   888  							ConfigDigest: "test config digest",
   889  						},
   890  						Realms: nil,
   891  					},
   892  				}
   893  				err = UpdateAuthProjectRealms(ctx, updatedExpandedRealms, "permissions.cfg:abc", false, "Go pRPC API")
   894  				So(err, ShouldBeNil)
   895  
   896  				actualChanges, err := generateChanges(ctx, 2, false)
   897  				So(err, ShouldBeNil)
   898  				validateChanges(ctx, "project realms changed", 2, actualChanges, []*AuthDBChange{{
   899  					ChangeType:   ChangeProjectRealmsChanged,
   900  					ConfigRevOld: "10001",
   901  					ConfigRevNew: "10002",
   902  				}})
   903  			})
   904  
   905  			Convey("realms changed but revisions identical", func() {
   906  				updatedExpandedRealms := []*ExpandedRealms{
   907  					{
   908  						CfgRev: &RealmsCfgRev{
   909  							ProjectID:    "proj1",
   910  							ConfigRev:    "10001",
   911  							ConfigDigest: "test config digest",
   912  						},
   913  						Realms: nil,
   914  					},
   915  				}
   916  				err = UpdateAuthProjectRealms(ctx, updatedExpandedRealms, "permissions.cfg:abc", false, "Go pRPC API")
   917  				So(err, ShouldBeNil)
   918  
   919  				actualChanges, err := generateChanges(ctx, 2, false)
   920  				So(err, ShouldBeNil)
   921  				validateChanges(ctx, "project realms changed", 2, actualChanges, []*AuthDBChange{{
   922  					ChangeType:   ChangeProjectRealmsChanged,
   923  					ConfigRevOld: "10001",
   924  					ConfigRevNew: "10001",
   925  				}})
   926  			})
   927  
   928  			Convey("both project config and permissions config changed", func() {
   929  				updatedProj1Realms := &protocol.Realms{
   930  					Permissions: makeTestPermissions("luci.dev.p2", "luci.dev.p1"),
   931  					Realms: []*protocol.Realm{
   932  						{
   933  							Name: "proj1:@root",
   934  							Bindings: []*protocol.Binding{
   935  								{
   936  									// Permissions p2, p1.
   937  									Permissions: []uint32{0, 1},
   938  									Principals:  []string{"group:gr1"},
   939  								},
   940  							},
   941  						},
   942  					},
   943  				}
   944  				updatedExpandedRealms := []*ExpandedRealms{
   945  					{
   946  						CfgRev: &RealmsCfgRev{
   947  							ProjectID:    "proj1",
   948  							ConfigRev:    "10002",
   949  							ConfigDigest: "test config digest",
   950  						},
   951  						Realms: updatedProj1Realms,
   952  					},
   953  				}
   954  				err = UpdateAuthProjectRealms(ctx, updatedExpandedRealms, "permissions.cfg:def", false, "Go pRPC API")
   955  				So(err, ShouldBeNil)
   956  
   957  				actualChanges, err := generateChanges(ctx, 2, false)
   958  				So(err, ShouldBeNil)
   959  				validateChanges(ctx, "project realms changed and reevaluated", 2, actualChanges, []*AuthDBChange{
   960  					{
   961  						ChangeType:   ChangeProjectRealmsChanged,
   962  						ConfigRevOld: "10001",
   963  						ConfigRevNew: "10002",
   964  					},
   965  					{
   966  						ChangeType:  ChangeProjectRealmsReevaluated,
   967  						PermsRevOld: "permissions.cfg:abc",
   968  						PermsRevNew: "permissions.cfg:def",
   969  					},
   970  				})
   971  			})
   972  		})
   973  
   974  		Convey("AuthRealmsGlobals changes", func() {
   975  			Convey("v1 permissions", func() {
   976  				// Helper function to mimic AuthRealmsGlobals being
   977  				// updated by Auth Service v1.
   978  				updateAuthRealmsGlobalsV1Perms := func(ctx context.Context, permissions []*protocol.Permission) error {
   979  					return runAuthDBChange(ctx, "mimicking Python update-realms cron", func(ctx context.Context, commitEntity commitAuthEntity) error {
   980  						stored, err := GetAuthRealmsGlobals(ctx)
   981  						if err != nil && !errors.Is(err, datastore.ErrNoSuchEntity) {
   982  							return errors.Annotate(err, "error while fetching AuthRealmsGlobals entity").Err()
   983  						}
   984  
   985  						if stored == nil {
   986  							stored = makeAuthRealmsGlobals(ctx)
   987  						}
   988  
   989  						perms := make([]string, len(permissions))
   990  						for i, p := range permissions {
   991  							perm, err := proto.Marshal(p)
   992  							if err != nil {
   993  								return err
   994  							}
   995  							perms[i] = string(perm)
   996  						}
   997  						stored.Permissions = perms
   998  						return commitEntity(stored, testModifiedTS, auth.CurrentIdentity(ctx), false)
   999  					})
  1000  				}
  1001  
  1002  				// Add permissions when there's no config.
  1003  				So(updateAuthRealmsGlobalsV1Perms(ctx, []*protocol.Permission{
  1004  					{Name: "test.perm.create"},
  1005  					{Name: "test.perm.edit"},
  1006  				}), ShouldBeNil)
  1007  				actualChanges, err := generateChanges(ctx, 1, false)
  1008  				So(err, ShouldBeNil)
  1009  				validateChanges(ctx, "update realms globals, old config not present", 1, actualChanges, []*AuthDBChange{{
  1010  					ChangeType:       ChangeRealmsGlobalsChanged,
  1011  					PermissionsAdded: []string{"test.perm.create", "test.perm.edit"},
  1012  				}})
  1013  
  1014  				// Modify the existing permissions.
  1015  				So(updateAuthRealmsGlobalsV1Perms(ctx, []*protocol.Permission{
  1016  					{Name: "test.perm.create", Internal: true},
  1017  				}), ShouldBeNil)
  1018  				actualChanges, err = generateChanges(ctx, 2, false)
  1019  				So(err, ShouldBeNil)
  1020  				validateChanges(ctx, "update realms globals, old config present", 2, actualChanges, []*AuthDBChange{{
  1021  					ChangeType:         ChangeRealmsGlobalsChanged,
  1022  					PermissionsChanged: []string{"test.perm.create"},
  1023  					PermissionsRemoved: []string{"test.perm.edit"},
  1024  				}})
  1025  			})
  1026  
  1027  			Convey("v2 permissions", func() {
  1028  				permCfg := &configspb.PermissionsConfig{
  1029  					Role: []*configspb.PermissionsConfig_Role{
  1030  						{
  1031  							Name: "role/test.role.editor",
  1032  							Permissions: []*protocol.Permission{
  1033  								{
  1034  									Name: "test.perm.edit",
  1035  								},
  1036  							},
  1037  						},
  1038  						{
  1039  							Name: "role/test.role.creator",
  1040  							Permissions: []*protocol.Permission{
  1041  								{
  1042  									Name: "test.perm.create",
  1043  								},
  1044  							},
  1045  						},
  1046  					},
  1047  				}
  1048  				So(UpdateAuthRealmsGlobals(ctx, permCfg, false, "Go pRPC API"), ShouldBeNil)
  1049  				So(taskScheduler.Tasks(), ShouldHaveLength, 2)
  1050  				actualChanges, err := generateChanges(ctx, 1, false)
  1051  				So(err, ShouldBeNil)
  1052  				validateChanges(ctx, "update realms globals, old config not present", 1, actualChanges, []*AuthDBChange{{
  1053  					ChangeType:       ChangeRealmsGlobalsChanged,
  1054  					PermissionsAdded: []string{"test.perm.create", "test.perm.edit"},
  1055  				}})
  1056  
  1057  				permCfg = &configspb.PermissionsConfig{
  1058  					Role: []*configspb.PermissionsConfig_Role{
  1059  						{
  1060  							Name: "role/test.role.creator",
  1061  							Permissions: []*protocol.Permission{
  1062  								{
  1063  									Name:     "test.perm.create",
  1064  									Internal: true,
  1065  								},
  1066  							},
  1067  						},
  1068  					},
  1069  				}
  1070  				So(UpdateAuthRealmsGlobals(ctx, permCfg, false, "Go pRPC API"), ShouldBeNil)
  1071  				So(taskScheduler.Tasks(), ShouldHaveLength, 4)
  1072  				actualChanges, err = generateChanges(ctx, 2, false)
  1073  				So(err, ShouldBeNil)
  1074  				validateChanges(ctx, "update realms globals, old config present", 2, actualChanges, []*AuthDBChange{{
  1075  					ChangeType:         ChangeRealmsGlobalsChanged,
  1076  					PermissionsChanged: []string{"test.perm.create"},
  1077  					PermissionsRemoved: []string{"test.perm.edit"},
  1078  				}})
  1079  			})
  1080  		})
  1081  
  1082  		Convey("Changelog generation cascades", func() {
  1083  			// Create and add 3 AuthGroups.
  1084  			groupNames := []string{"group-1", "group-2", "group-3"}
  1085  			for _, groupName := range groupNames {
  1086  				ag := makeAuthGroup(ctx, groupName)
  1087  				_, err := CreateAuthGroup(ctx, ag, false, "Go pRPC API", false)
  1088  				So(err, ShouldBeNil)
  1089  			}
  1090  			// There should be a changelog task and replication task for
  1091  			// each group.
  1092  			So(taskScheduler.Tasks(), ShouldHaveLength, 6)
  1093  
  1094  			actualChanges, err := generateChanges(ctx, 1, false)
  1095  			So(err, ShouldBeNil)
  1096  			validateChanges(ctx, "created first group in cascade test", 1, actualChanges, []*AuthDBChange{{
  1097  				ChangeType: ChangeGroupCreated,
  1098  				Owners:     AdminGroup,
  1099  			}})
  1100  			// First change has no prior revision, so a task should not
  1101  			// have been added.
  1102  			So(taskScheduler.Tasks(), ShouldHaveLength, 6)
  1103  
  1104  			actualChanges, err = generateChanges(ctx, 3, false)
  1105  			So(err, ShouldBeNil)
  1106  			validateChanges(ctx, "created third group in cascade test", 3, actualChanges, []*AuthDBChange{{
  1107  				ChangeType: ChangeGroupCreated,
  1108  				Owners:     AdminGroup,
  1109  			}})
  1110  			// Changelog for rev 2 has not been generated yet, so there
  1111  			// should be an added task.
  1112  			So(taskScheduler.Tasks(), ShouldHaveLength, 7)
  1113  
  1114  			actualChanges, err = generateChanges(ctx, 2, false)
  1115  			So(err, ShouldBeNil)
  1116  			validateChanges(ctx, "created second group in cascade test", 2, actualChanges, []*AuthDBChange{{
  1117  				ChangeType: ChangeGroupCreated,
  1118  				Owners:     AdminGroup,
  1119  			}})
  1120  			// Changelog for rev 1 has already been generated, so a task
  1121  			// should not have been added.
  1122  			So(taskScheduler.Tasks(), ShouldHaveLength, 7)
  1123  		})
  1124  
  1125  		Convey("dry run of changelog generation works", func() {
  1126  			agDryRun := makeAuthGroup(ctx, "group-dry-run")
  1127  			_, err := CreateAuthGroup(ctx, agDryRun, false, "Go pRPC API", false)
  1128  			So(err, ShouldBeNil)
  1129  			So(taskScheduler.Tasks(), ShouldHaveLength, 2)
  1130  			_, err = generateChanges(ctx, 1, true)
  1131  			So(err, ShouldBeNil)
  1132  
  1133  			// Check the changelog was created in dry run mode.
  1134  			actualChanges := getChanges(ctx, 1, true)
  1135  			So(actualChanges, ShouldHaveLength, 1)
  1136  			So(actualChanges[0].Kind, ShouldEqual, "V2AuthDBChange")
  1137  			So(actualChanges[0].Parent, ShouldEqual, ChangeLogRevisionKey(ctx, 1, true))
  1138  			So(actualChanges[0].ChangeType, ShouldEqual, ChangeGroupCreated)
  1139  			So(actualChanges[0].Owners, ShouldEqual, AdminGroup)
  1140  
  1141  			// Check there were no changes written with dry run mode off.
  1142  			So(getChanges(ctx, 1, false), ShouldBeEmpty)
  1143  		})
  1144  	})
  1145  }
  1146  
  1147  func TestPropertyMapHelpers(t *testing.T) {
  1148  	Convey("getStringSliceProp robustly returns strings", t, func() {
  1149  		pm := datastore.PropertyMap{}
  1150  		pm["testField"] = datastore.PropertySlice{
  1151  			datastore.MkPropertyNI([]byte("test data first entry")),
  1152  			datastore.MkPropertyNI("test data second entry"),
  1153  		}
  1154  
  1155  		So(getStringSliceProp(pm, "testField"), ShouldEqual, []string{
  1156  			"test data first entry",
  1157  			"test data second entry",
  1158  		})
  1159  	})
  1160  }