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  }