go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/impl/model/importer_test.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package model 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "sort" 22 "strings" 23 "testing" 24 "time" 25 26 "go.chromium.org/luci/auth/identity" 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/data/stringset" 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/tq" 36 37 "go.chromium.org/luci/auth_service/impl/info" 38 "go.chromium.org/luci/auth_service/testsupport" 39 40 . "github.com/smartystreets/goconvey/convey" 41 . "go.chromium.org/luci/common/testing/assertions" 42 ) 43 44 func testGroupImporterConfig() *GroupImporterConfig { 45 return &GroupImporterConfig{ 46 Kind: "GroupImporterConfig", 47 ID: "config", 48 ConfigProto: ` 49 # Schema for this file: 50 # https://config.luci.app/schemas/services/chrome-infra-auth:imports.cfg 51 # See GroupImporterConfig message. 52 53 # Groups pushed by //depot/google3/googleclient/chrome/infra/groups_push_cron/ 54 # 55 # To add new groups, see README.md there. 56 57 tarball_upload { 58 name: "test_groups.tar.gz" 59 authorized_uploader: "test-push-cron@system.example.com" 60 systems: "tst" 61 } 62 63 tarball_upload { 64 name: "example_groups.tar.gz" 65 authorized_uploader: "another-push-cron@system.example.com" 66 systems: "examp" 67 } 68 `, 69 ConfigRevision: []byte("some-config-revision"), 70 ModifiedBy: "some-user@example.com", 71 ModifiedTS: testModifiedTS, 72 } 73 } 74 func TestGroupImporterConfigModel(t *testing.T) { 75 t.Parallel() 76 ctx := memory.Use(context.Background()) 77 78 Convey("testing GetGroupImporterConfig", t, func() { 79 groupCfg := testGroupImporterConfig() 80 81 _, err := GetGroupImporterConfig(ctx) 82 So(err, ShouldEqual, datastore.ErrNoSuchEntity) 83 84 So(datastore.Put(ctx, groupCfg), ShouldBeNil) 85 86 actual, err := GetGroupImporterConfig(ctx) 87 So(err, ShouldBeNil) 88 So(actual, ShouldResemble, groupCfg) 89 }) 90 } 91 92 func TestLoadGroupFile(t *testing.T) { 93 t.Parallel() 94 testDomain := "example.com" 95 96 Convey("Testing LoadGroupFile()", t, func() { 97 Convey("OK", func() { 98 body := strings.Join([]string{"", "b", "a", "a", ""}, "\n") 99 aIdent, _ := identity.MakeIdentity(fmt.Sprintf("user:a@%s", testDomain)) 100 bIdent, _ := identity.MakeIdentity(fmt.Sprintf("user:b@%s", testDomain)) 101 102 actual, err := loadGroupFile(body, testDomain) 103 So(err, ShouldBeNil) 104 So(actual, ShouldResemble, []identity.Identity{ 105 aIdent, 106 bIdent, 107 }) 108 }) 109 Convey("bad id", func() { 110 body := "bad id" 111 _, err := loadGroupFile(body, testDomain) 112 So(err, ShouldErrLike, `auth: bad value "bad id@example.com" for identity kind "user"`) 113 }) 114 }) 115 } 116 117 func TestExtractTarArchive(t *testing.T) { 118 t.Parallel() 119 Convey("valid tarball with skippable files", t, func() { 120 expected := map[string][]byte{ 121 "at_root": []byte("a\nb"), 122 "ldap/ bad name": []byte("a\nb"), 123 "ldap/group-a": []byte("a\nb"), 124 "ldap/group-b": []byte("a\nb"), 125 "ldap/group-c": []byte("a\nb"), 126 "ldap/deeper/group-a": []byte("a\nb"), 127 "not-ldap/group-a": []byte("a\nb"), 128 } 129 bundle := testsupport.BuildTargz(expected) 130 entries, err := extractTarArchive(bytes.NewReader(bundle)) 131 So(err, ShouldBeNil) 132 So(entries, ShouldResemble, expected) 133 }) 134 } 135 136 func TestLoadTarball(t *testing.T) { 137 t.Parallel() 138 ctx := memory.Use(context.Background()) 139 140 Convey("testing loadTarball", t, func() { 141 Convey("invalid tarball bad identity", func() { 142 bundle := testsupport.BuildTargz(map[string][]byte{ 143 "at_root": []byte("a\nb"), 144 "ldap/group-a": []byte("a\n!!!!!!"), 145 }) 146 _, err := loadTarball(ctx, bytes.NewReader(bundle), "example.com", []string{"ldap"}, []string{"ldap/group-a", "ldap/group-b"}) 147 So(err, ShouldErrLike, `auth: bad value "!!!!!!@example.com" for identity kind "user"`) 148 }) 149 Convey("valid tarball with skippable files", func() { 150 bundle := testsupport.BuildTargz(map[string][]byte{ 151 "at_root": []byte("a\nb"), 152 "ldap/ bad name": []byte("a\nb"), 153 "ldap/group-a": []byte("a\nb"), 154 "ldap/group-b": []byte("a\nb"), 155 "ldap/group-c": []byte("a\nb"), 156 "ldap/deeper/group-a": []byte("a\nb"), 157 "not-ldap/group-a": []byte("a\nb"), 158 }) 159 m, err := loadTarball(ctx, bytes.NewReader(bundle), "example.com", []string{"ldap"}, []string{"ldap/group-a", "ldap/group-b"}) 160 So(err, ShouldBeNil) 161 aIdent, _ := identity.MakeIdentity("user:a@example.com") 162 bIdent, _ := identity.MakeIdentity("user:b@example.com") 163 So(m, ShouldResemble, map[string]GroupBundle{ 164 "ldap": { 165 "ldap/group-a": { 166 aIdent, 167 bIdent, 168 }, 169 "ldap/group-b": { 170 aIdent, 171 bIdent, 172 }, 173 }, 174 }) 175 }) 176 }) 177 } 178 179 func TestIngestTarball(t *testing.T) { 180 t.Parallel() 181 ctx := auth.WithState(memory.Use(context.Background()), &authtest.FakeState{ 182 Identity: "user:test-push-cron@system.example.com", 183 }) 184 ctx = clock.Set(ctx, testclock.New(testCreatedTS)) 185 ctx = info.SetImageVersion(ctx, "test-version") 186 ctx, taskScheduler := tq.TestingContext(txndefer.FilterRDS(ctx), nil) 187 188 cfg := testGroupImporterConfig() 189 190 bundle := testsupport.BuildTargz(map[string][]byte{ 191 "at_root": []byte("a\nb"), 192 "tst/ bad name": []byte("a\nb"), 193 "tst/group-a": []byte("a@example.com\nb@example.test.com"), 194 "tst/group-b": []byte("a@example.com"), 195 "tst/group-c": []byte("a@example.com\nc@test-example.com"), 196 "tst/deeper/group-a": []byte("a\nb"), 197 "not-tst/group-a": []byte("a\nb"), 198 }) 199 200 Convey("testing IngestTarball", t, func() { 201 Convey("unknown", func() { 202 So(datastore.Put(ctx, cfg), ShouldBeNil) 203 _, _, err := IngestTarball(ctx, "zzz", nil) 204 So(err, ShouldErrLike, "entry is nil") 205 }) 206 Convey("unauthorized", func() { 207 badAuthCtx := auth.WithState(ctx, &authtest.FakeState{ 208 Identity: "user:someone@example.com", 209 }) 210 _, _, err := IngestTarball(badAuthCtx, "test_groups.tar.gz", bytes.NewReader(bundle)) 211 So(err, ShouldErrLike, `"someone@example.com" is not an authorized uploader`) 212 }) 213 Convey("not configured", func() { 214 badCtx := memory.Use(context.Background()) 215 _, _, err := IngestTarball(badCtx, "", nil) 216 So(err, ShouldErrLike, datastore.ErrNoSuchEntity) 217 }) 218 Convey("happy", func() { 219 g := makeAuthGroup(ctx, "administrators") 220 g.AuthVersionedEntityMixin = testAuthVersionedEntityMixin() 221 _, err := CreateAuthGroup(ctx, g, false, "Imported from group bundles", false) 222 So(err, ShouldBeNil) 223 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 224 updatedGroups, revision, err := IngestTarball(ctx, "test_groups.tar.gz", bytes.NewReader(bundle)) 225 So(err, ShouldBeNil) 226 So(revision, ShouldEqual, 2) 227 So(updatedGroups, ShouldResemble, []string{ 228 "tst/group-a", 229 "tst/group-b", 230 "tst/group-c", 231 }) 232 }) 233 }) 234 } 235 236 func TestImportBundles(t *testing.T) { 237 t.Parallel() 238 239 Convey("Testing importBundles", t, func() { 240 userIdent := identity.Identity("user:test-modifier@example.com") 241 ctx := memory.Use(context.Background()) 242 tc := testclock.New(testModifiedTS) 243 ctx = clock.Set(ctx, tc) 244 tc.SetTimerCallback(func(d time.Duration, t clock.Timer) { 245 tc.Add(d) 246 }) 247 ctx = info.SetImageVersion(ctx, "test-version") 248 ctx, taskScheduler := tq.TestingContext(txndefer.FilterRDS(ctx), nil) 249 250 adminGroup := emptyAuthGroup(ctx, AdminGroup) 251 So(datastore.Put(ctx, adminGroup), ShouldBeNil) 252 253 aIdent, _ := identity.MakeIdentity("user:a@example.com") 254 255 sGroupA := testExternalAuthGroup(ctx, "sys/group-a", []string{string(aIdent)}) 256 257 sGroupB := testExternalAuthGroup(ctx, "sys/group-b", []string{string(aIdent)}) 258 sGroupC := testExternalAuthGroup(ctx, "sys/group-c", []string{string(aIdent)}) 259 260 eGroupA := testExternalAuthGroup(ctx, "ext/group-a", []string{string(aIdent)}) 261 262 bundles := map[string]GroupBundle{ 263 "ext": { 264 eGroupA.ID: { 265 aIdent, 266 }, 267 }, 268 "sys": { 269 sGroupA.ID: { 270 aIdent, 271 }, 272 sGroupB.ID: { 273 aIdent, 274 }, 275 sGroupC.ID: { 276 aIdent, 277 }, 278 }, 279 } 280 281 baseSlice := []string{eGroupA.ID, sGroupA.ID, sGroupB.ID, sGroupC.ID} 282 baseGroupBundles := stringset.NewFromSlice(baseSlice...).ToSortedSlice() 283 284 Convey("Creating groups", func() { 285 updatedGroups, revision, err := importBundles(ctx, bundles, userIdent, nil) 286 So(err, ShouldBeNil) 287 So(updatedGroups, ShouldResemble, baseGroupBundles) 288 So(revision, ShouldEqual, 1) 289 groupA, err := GetAuthGroup(ctx, sGroupA.ID) 290 So(err, ShouldBeNil) 291 So(groupA.Members, ShouldResemble, sGroupA.Members) 292 293 groupB, err := GetAuthGroup(ctx, sGroupB.ID) 294 So(err, ShouldBeNil) 295 So(groupB.Members, ShouldResemble, sGroupB.Members) 296 297 groupC, err := GetAuthGroup(ctx, sGroupC.ID) 298 So(err, ShouldBeNil) 299 So(groupC.Members, ShouldResemble, sGroupC.Members) 300 301 groupAe, err := GetAuthGroup(ctx, eGroupA.ID) 302 So(err, ShouldBeNil) 303 So(groupAe.Members, ShouldResemble, eGroupA.Members) 304 305 So(taskScheduler.Tasks(), ShouldHaveLength, 2) 306 }) 307 308 Convey("Updating Groups", func() { 309 g := testExternalAuthGroup(ctx, "sys/group-a", []string{"user:b@example.com", "user:c@example.com"}) 310 _, err := CreateAuthGroup(ctx, g, true, "Imported from group bundles", false) 311 So(err, ShouldBeNil) 312 group, err := GetAuthGroup(ctx, sGroupA.ID) 313 So(err, ShouldBeNil) 314 So(group.Members, ShouldResemble, g.Members) 315 updatedGroups, revision, err := importBundles(ctx, bundles, userIdent, nil) 316 So(err, ShouldBeNil) 317 So(updatedGroups, ShouldResemble, baseGroupBundles) 318 So(revision, ShouldEqual, 2) 319 group, err = GetAuthGroup(ctx, sGroupA.ID) 320 So(err, ShouldBeNil) 321 So(group.Members, ShouldResemble, sGroupA.Members) 322 }) 323 324 Convey("Deleting Groups", func() { 325 g := testExternalAuthGroup(ctx, "sys/group-d", []string{"user:a@example.com"}) 326 _, err := CreateAuthGroup(ctx, g, true, "Imported from group bundles", false) 327 So(err, ShouldBeNil) 328 329 grDS, err := GetAuthGroup(ctx, g.ID) 330 So(err, ShouldBeNil) 331 So(grDS.Members, ShouldResemble, g.Members) 332 333 updatedGroups, revision, err := importBundles(ctx, bundles, userIdent, nil) 334 So(err, ShouldBeNil) 335 So(updatedGroups, ShouldResemble, append(baseGroupBundles, "sys/group-d")) 336 So(revision, ShouldEqual, 2) 337 338 _, err = GetAuthGroup(ctx, g.ID) 339 So(err, ShouldErrLike, datastore.ErrNoSuchEntity) 340 }) 341 342 Convey("Large groups", func() { 343 bundle, groupsBundled := makeGroupBundle("test", 400) 344 updatedGroups, rev, err := importBundles(ctx, bundle, userIdent, nil) 345 So(err, ShouldBeNil) 346 So(updatedGroups, ShouldResemble, groupsBundled) 347 So(rev, ShouldEqual, 2) 348 So(taskScheduler.Tasks(), ShouldHaveLength, 4) 349 groups, err := GetAllAuthGroups(ctx) 350 So(err, ShouldBeNil) 351 So(groups, ShouldHaveLength, 401) 352 }) 353 354 Convey("Revision changes in between transactions", func() { 355 bundle, groupsBundled := makeGroupBundle("test", 500) 356 updatedGroups, rev, err := importBundles(ctx, bundle, userIdent, func() { 357 So(datastore.Put(ctx, testAuthReplicationState(ctx, 3)), ShouldBeNil) 358 }) 359 So(err, ShouldBeNil) 360 So(updatedGroups, ShouldResemble, groupsBundled) 361 So(rev, ShouldEqual, 5) 362 }) 363 364 Convey("Large put Large delete", func() { 365 bundle, groupsBundledTest := makeGroupBundle("test", 1000) 366 updatedGroups, rev, err := importBundles(ctx, bundle, userIdent, func() {}) 367 So(err, ShouldBeNil) 368 So(updatedGroups, ShouldResemble, groupsBundledTest) 369 So(rev, ShouldEqual, 5) 370 bundle, groupsBundled := makeGroupBundle("example", 400) 371 updatedGroups, rev, err = importBundles(ctx, bundle, userIdent, func() {}) 372 So(err, ShouldBeNil) 373 So(updatedGroups, ShouldResemble, groupsBundled) 374 So(rev, ShouldEqual, 7) 375 376 bundle, groupsBundled = makeGroupBundle("tst", 500) 377 bundle["test"] = GroupBundle{} 378 groupsBundled = append(groupsBundled, groupsBundledTest...) 379 updatedGroups, rev, err = importBundles(ctx, bundle, userIdent, func() {}) 380 So(err, ShouldBeNil) 381 sort.Strings(groupsBundled) 382 So(updatedGroups, ShouldResemble, groupsBundled) 383 So(rev, ShouldEqual, 15) 384 }) 385 386 // The max number of create, update, or delete in one transaction 387 // is 500. For every entity we modify we attach a history entity 388 // in the code for importing we limit this to 200 so we can be 389 // under the limit by only touching 400 entities. 390 Convey("150 put 150 del", func() { 391 bundle, groupsBundledTest := makeGroupBundle("test", 150) 392 updatedGroups, rev, err := importBundles(ctx, bundle, userIdent, func() {}) 393 So(err, ShouldBeNil) 394 So(updatedGroups, ShouldResemble, groupsBundledTest) 395 So(rev, ShouldEqual, 1) 396 bundle, groupsBundled := makeGroupBundle("tst", 150) 397 bundle["test"] = GroupBundle{} 398 groupsBundled = append(groupsBundled, groupsBundledTest...) 399 sort.Strings(groupsBundled) 400 updatedGroups, rev, err = importBundles(ctx, bundle, userIdent, func() {}) 401 So(err, ShouldBeNil) 402 So(updatedGroups, ShouldResemble, groupsBundled) 403 So(rev, ShouldEqual, 3) 404 }) 405 }) 406 } 407 408 func makeGroupBundle(system string, size int) (map[string]GroupBundle, []string) { 409 bundle := map[string]GroupBundle{} 410 groupsBundled := stringset.New(0) 411 bundle[system] = make(GroupBundle, size) 412 for i := 0; i < size; i++ { 413 group := fmt.Sprintf("%s/group-%d", system, i) 414 bundle[system][group] = []identity.Identity{"user:a@example.com"} 415 groupsBundled.Add(group) 416 } 417 return bundle, groupsBundled.ToSortedSlice() 418 }