go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/authdb/snapshot_test.go (about)

     1  // Copyright 2016 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 authdb
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"flag"
    21  	"math/rand"
    22  	"net"
    23  	"net/http"
    24  	"os"
    25  	"runtime"
    26  	"sort"
    27  	"strings"
    28  	"testing"
    29  
    30  	"google.golang.org/protobuf/proto"
    31  
    32  	"go.chromium.org/luci/auth/identity"
    33  	"go.chromium.org/luci/common/data/stringset"
    34  
    35  	"go.chromium.org/luci/server/auth/authdb/internal/graph"
    36  	"go.chromium.org/luci/server/auth/authdb/internal/legacy"
    37  	"go.chromium.org/luci/server/auth/authdb/internal/oauthid"
    38  	"go.chromium.org/luci/server/auth/authdb/internal/realmset"
    39  	"go.chromium.org/luci/server/auth/internal"
    40  	"go.chromium.org/luci/server/auth/realms"
    41  	"go.chromium.org/luci/server/auth/service/protocol"
    42  	"go.chromium.org/luci/server/auth/signing"
    43  	"go.chromium.org/luci/server/auth/signing/signingtest"
    44  	"go.chromium.org/luci/server/caching"
    45  
    46  	. "github.com/smartystreets/goconvey/convey"
    47  	. "go.chromium.org/luci/common/testing/assertions"
    48  )
    49  
    50  var (
    51  	perm1       = realms.RegisterPermission("luci.dev.testing1")
    52  	perm2       = realms.RegisterPermission("luci.dev.testing2")
    53  	unknownPerm = realms.RegisterPermission("luci.dev.unknown")
    54  
    55  	// For the benchmark that uses real AuthDB dumps.
    56  	bbBuildsGet = realms.RegisterPermission("buildbucket.builds.get")
    57  )
    58  
    59  func init() {
    60  	perm1.AddFlags(realms.UsedInQueryRealms)
    61  	bbBuildsGet.AddFlags(realms.UsedInQueryRealms)
    62  }
    63  
    64  func TestSnapshotDB(t *testing.T) {
    65  	c := context.Background()
    66  
    67  	securityConfig, _ := proto.Marshal(&protocol.SecurityConfig{
    68  		InternalServiceRegexp: []string{
    69  			`(.*-dot-)?i1\.example\.com`,
    70  			`(.*-dot-)?i2\.example\.com`,
    71  		},
    72  	})
    73  
    74  	db, err := NewSnapshotDB(&protocol.AuthDB{
    75  		OauthClientId: "primary-client-id",
    76  		OauthAdditionalClientIds: []string{
    77  			"additional-client-id-1",
    78  			"additional-client-id-2",
    79  		},
    80  		TokenServerUrl: "http://token-server",
    81  		Groups: []*protocol.AuthGroup{
    82  			{
    83  				Name:    "direct",
    84  				Members: []string{"user:abc@example.com"},
    85  			},
    86  			{
    87  				Name:  "via glob",
    88  				Globs: []string{"user:*@example.com"},
    89  			},
    90  			{
    91  				Name:   "via nested",
    92  				Nested: []string{"direct"},
    93  			},
    94  			{
    95  				Name:   "cycle",
    96  				Nested: []string{"cycle"},
    97  			},
    98  			{
    99  				Name:   "unknown nested",
   100  				Nested: []string{"unknown"},
   101  			},
   102  			{
   103  				Name: "empty",
   104  			},
   105  		},
   106  		IpWhitelistAssignments: []*protocol.AuthIPWhitelistAssignment{
   107  			{
   108  				Identity:    "user:abc@example.com",
   109  				IpWhitelist: "allowlist",
   110  			},
   111  		},
   112  		IpWhitelists: []*protocol.AuthIPWhitelist{
   113  			{
   114  				Name: "allowlist",
   115  				Subnets: []string{
   116  					"1.2.3.4/32",
   117  					"10.0.0.0/8",
   118  				},
   119  			},
   120  			{
   121  				Name: "empty",
   122  			},
   123  		},
   124  		SecurityConfig: securityConfig,
   125  	}, "http://auth-service", 1234, false)
   126  	if err != nil {
   127  		panic(err)
   128  	}
   129  
   130  	Convey("IsAllowedOAuthClientID works", t, func() {
   131  		call := func(email, clientID string) bool {
   132  			res, err := db.IsAllowedOAuthClientID(c, email, clientID)
   133  			So(err, ShouldBeNil)
   134  			return res
   135  		}
   136  
   137  		So(call("dude@example.com", ""), ShouldBeFalse)
   138  		So(call("dude@example.com", oauthid.GoogleAPIExplorerClientID), ShouldBeTrue)
   139  		So(call("dude@example.com", "primary-client-id"), ShouldBeTrue)
   140  		So(call("dude@example.com", "additional-client-id-2"), ShouldBeTrue)
   141  		So(call("dude@example.com", "unknown-client-id"), ShouldBeFalse)
   142  	})
   143  
   144  	Convey("IsInternalService works", t, func() {
   145  		call := func(hostname string) bool {
   146  			res, err := db.IsInternalService(c, hostname)
   147  			So(err, ShouldBeNil)
   148  			return res
   149  		}
   150  
   151  		So(call("i1.example.com"), ShouldBeTrue)
   152  		So(call("i2.example.com"), ShouldBeTrue)
   153  		So(call("abc-dot-i1.example.com"), ShouldBeTrue)
   154  		So(call("external.example.com"), ShouldBeFalse)
   155  		So(call("something-i1.example.com"), ShouldBeFalse)
   156  		So(call("i1.example.com-something"), ShouldBeFalse)
   157  	})
   158  
   159  	Convey("IsMember works", t, func() {
   160  		call := func(ident string, groups ...string) bool {
   161  			res, err := db.IsMember(c, identity.Identity(ident), groups)
   162  			So(err, ShouldBeNil)
   163  			return res
   164  		}
   165  
   166  		So(call("user:abc@example.com", "direct"), ShouldBeTrue)
   167  		So(call("user:another@example.com", "direct"), ShouldBeFalse)
   168  
   169  		So(call("user:abc@example.com", "via glob"), ShouldBeTrue)
   170  		So(call("user:abc@another.com", "via glob"), ShouldBeFalse)
   171  
   172  		So(call("user:abc@example.com", "via nested"), ShouldBeTrue)
   173  		So(call("user:another@example.com", "via nested"), ShouldBeFalse)
   174  
   175  		So(call("user:abc@example.com", "cycle"), ShouldBeFalse)
   176  		So(call("user:abc@example.com", "unknown"), ShouldBeFalse)
   177  		So(call("user:abc@example.com", "unknown nested"), ShouldBeFalse)
   178  
   179  		So(call("user:abc@example.com"), ShouldBeFalse)
   180  		So(call("user:abc@example.com", "unknown", "direct"), ShouldBeTrue)
   181  		So(call("user:abc@example.com", "via glob", "direct"), ShouldBeTrue)
   182  	})
   183  
   184  	Convey("CheckMembership works", t, func() {
   185  		call := func(ident string, groups ...string) []string {
   186  			res, err := db.CheckMembership(c, identity.Identity(ident), groups)
   187  			So(err, ShouldBeNil)
   188  			return res
   189  		}
   190  
   191  		So(call("user:abc@example.com", "direct"), ShouldResemble, []string{"direct"})
   192  		So(call("user:another@example.com", "direct"), ShouldBeNil)
   193  
   194  		So(call("user:abc@example.com", "via glob"), ShouldResemble, []string{"via glob"})
   195  		So(call("user:abc@another.com", "via glob"), ShouldBeNil)
   196  
   197  		So(call("user:abc@example.com", "via nested"), ShouldResemble, []string{"via nested"})
   198  		So(call("user:another@example.com", "via nested"), ShouldBeNil)
   199  
   200  		So(call("user:abc@example.com", "cycle"), ShouldBeNil)
   201  		So(call("user:abc@example.com", "unknown"), ShouldBeNil)
   202  		So(call("user:abc@example.com", "unknown nested"), ShouldBeNil)
   203  
   204  		So(call("user:abc@example.com"), ShouldBeNil)
   205  		So(call("user:abc@example.com", "unknown", "direct"), ShouldResemble, []string{"direct"})
   206  		So(call("user:abc@example.com", "via glob", "direct"), ShouldResemble, []string{"via glob", "direct"})
   207  	})
   208  
   209  	Convey("FilterKnownGroups works", t, func() {
   210  		known, err := db.FilterKnownGroups(c, []string{"direct", "unknown", "empty", "direct", "unknown"})
   211  		So(err, ShouldBeNil)
   212  		So(known, ShouldResemble, []string{"direct", "empty", "direct"})
   213  	})
   214  
   215  	Convey("With realms", t, func() {
   216  		db, err := NewSnapshotDB(&protocol.AuthDB{
   217  			Groups: []*protocol.AuthGroup{
   218  				{
   219  					Name:    "direct",
   220  					Members: []string{"user:abc@example.com"},
   221  				},
   222  			},
   223  			Realms: &protocol.Realms{
   224  				ApiVersion: realmset.ExpectedAPIVersion,
   225  				Permissions: []*protocol.Permission{
   226  					{Name: perm1.Name()},
   227  					{Name: perm2.Name()},
   228  				},
   229  				Conditions: []*protocol.Condition{
   230  					{
   231  						Op: &protocol.Condition_Restrict{
   232  							Restrict: &protocol.Condition_AttributeRestriction{
   233  								Attribute: "a",
   234  								Values:    []string{"ok_1", "ok_2"},
   235  							},
   236  						},
   237  					},
   238  				},
   239  				Realms: []*protocol.Realm{
   240  					{
   241  						Name: "proj:@root",
   242  						Bindings: []*protocol.Binding{
   243  							{
   244  								Permissions: []uint32{0},
   245  								Principals:  []string{"user:root@example.com"},
   246  							},
   247  						},
   248  						Data: &protocol.RealmData{
   249  							EnforceInService: []string{"root"},
   250  						},
   251  					},
   252  					{
   253  						Name: "proj:some/realm",
   254  						Bindings: []*protocol.Binding{
   255  							{
   256  								Permissions: []uint32{0},
   257  								Principals:  []string{"user:realm@example.com", "group:direct"},
   258  							},
   259  							{
   260  								Conditions:  []uint32{0},
   261  								Permissions: []uint32{0},
   262  								Principals:  []string{"user:cond@example.com"},
   263  							},
   264  						},
   265  						Data: &protocol.RealmData{
   266  							EnforceInService: []string{"some"},
   267  						},
   268  					},
   269  					{
   270  						Name: "another:realm",
   271  						Bindings: []*protocol.Binding{
   272  							{
   273  								Permissions: []uint32{0},
   274  								Principals:  []string{"user:realm@example.com"},
   275  							},
   276  						},
   277  					},
   278  					{
   279  						Name: "proj:empty",
   280  					},
   281  				},
   282  			},
   283  		}, "http://auth-service", 1234, false)
   284  		So(err, ShouldBeNil)
   285  
   286  		Convey("HasPermission works", func() {
   287  			// A direct hit.
   288  			ok, err := db.HasPermission(c, "user:realm@example.com", perm1, "proj:some/realm", nil)
   289  			So(err, ShouldBeNil)
   290  			So(ok, ShouldBeTrue)
   291  
   292  			// A hit through a group.
   293  			ok, err = db.HasPermission(c, "user:abc@example.com", perm1, "proj:some/realm", nil)
   294  			So(err, ShouldBeNil)
   295  			So(ok, ShouldBeTrue)
   296  
   297  			// Fallback to the root.
   298  			ok, err = db.HasPermission(c, "user:root@example.com", perm1, "proj:unknown", nil)
   299  			So(err, ShouldBeNil)
   300  			So(ok, ShouldBeTrue)
   301  
   302  			// No permission.
   303  			ok, err = db.HasPermission(c, "user:realm@example.com", perm2, "proj:some/realm", nil)
   304  			So(err, ShouldBeNil)
   305  			So(ok, ShouldBeFalse)
   306  
   307  			// Unknown root realm.
   308  			ok, err = db.HasPermission(c, "user:realm@example.com", perm1, "unknown:@root", nil)
   309  			So(err, ShouldBeNil)
   310  			So(ok, ShouldBeFalse)
   311  
   312  			// Unknown permission.
   313  			ok, err = db.HasPermission(c, "user:realm@example.com", unknownPerm, "proj:some/realm", nil)
   314  			So(err, ShouldBeNil)
   315  			So(ok, ShouldBeFalse)
   316  
   317  			// Empty realm.
   318  			ok, err = db.HasPermission(c, "user:realm@example.com", perm1, "proj:empty", nil)
   319  			So(err, ShouldBeNil)
   320  			So(ok, ShouldBeFalse)
   321  
   322  			// Invalid realm name.
   323  			_, err = db.HasPermission(c, "user:realm@example.com", perm1, "@root", nil)
   324  			So(err, ShouldErrLike, "bad global realm name")
   325  		})
   326  
   327  		Convey("Conditional bindings", func() {
   328  			ok, err := db.HasPermission(c, "user:cond@example.com", perm1, "proj:some/realm", nil)
   329  			So(err, ShouldBeNil)
   330  			So(ok, ShouldBeFalse)
   331  
   332  			ok, err = db.HasPermission(c, "user:cond@example.com", perm1, "proj:some/realm", realms.Attrs{"a": "ok_1"})
   333  			So(err, ShouldBeNil)
   334  			So(ok, ShouldBeTrue)
   335  
   336  			ok, err = db.HasPermission(c, "user:cond@example.com", perm1, "proj:some/realm", realms.Attrs{"a": "ok_2"})
   337  			So(err, ShouldBeNil)
   338  			So(ok, ShouldBeTrue)
   339  
   340  			ok, err = db.HasPermission(c, "user:cond@example.com", perm1, "proj:some/realm", realms.Attrs{"a": "???"})
   341  			So(err, ShouldBeNil)
   342  			So(ok, ShouldBeFalse)
   343  
   344  			ok, err = db.HasPermission(c, "user:cond@example.com", perm2, "proj:some/realm", realms.Attrs{"a": "ok_1"})
   345  			So(err, ShouldBeNil)
   346  			So(ok, ShouldBeFalse)
   347  		})
   348  
   349  		Convey("QueryRealms works", func() {
   350  			// A direct hit.
   351  			r, err := db.QueryRealms(c, "user:realm@example.com", perm1, "", nil)
   352  			sort.Strings(r)
   353  			So(err, ShouldBeNil)
   354  			So(r, ShouldResemble, []string{"another:realm", "proj:some/realm"})
   355  
   356  			// Filtering by project.
   357  			r, err = db.QueryRealms(c, "user:realm@example.com", perm1, "proj", nil)
   358  			So(err, ShouldBeNil)
   359  			So(r, ShouldResemble, []string{"proj:some/realm"})
   360  
   361  			// A hit through a group.
   362  			r, err = db.QueryRealms(c, "user:abc@example.com", perm1, "", nil)
   363  			So(err, ShouldBeNil)
   364  			So(r, ShouldResemble, []string{"proj:some/realm"})
   365  		})
   366  
   367  		Convey("QueryRealms with conditional bindings", func() {
   368  			r, err := db.QueryRealms(c, "user:cond@example.com", perm1, "", nil)
   369  			So(err, ShouldBeNil)
   370  			So(r, ShouldBeEmpty)
   371  
   372  			r, err = db.QueryRealms(c, "user:cond@example.com", perm1, "", realms.Attrs{"a": "ok_1"})
   373  			So(err, ShouldBeNil)
   374  			So(r, ShouldResemble, []string{"proj:some/realm"})
   375  
   376  			r, err = db.QueryRealms(c, "user:cond@example.com", perm1, "", realms.Attrs{"a": "???"})
   377  			So(err, ShouldBeNil)
   378  			So(r, ShouldBeEmpty)
   379  		})
   380  
   381  		Convey("QueryRealms with unindexed permission", func() {
   382  			_, err := db.QueryRealms(c, "user:realm@example.com", perm2, "", nil)
   383  			So(err, ShouldErrLike, "permission luci.dev.testing2 cannot be used in QueryRealms")
   384  		})
   385  
   386  		Convey("GetRealmData works", func() {
   387  			// Known realm with data.
   388  			d, err := db.GetRealmData(c, "proj:some/realm")
   389  			So(err, ShouldBeNil)
   390  			So(d, ShouldResembleProto, &protocol.RealmData{
   391  				EnforceInService: []string{"some"},
   392  			})
   393  
   394  			// Known realm, with no data.
   395  			d, err = db.GetRealmData(c, "proj:empty")
   396  			So(err, ShouldBeNil)
   397  			So(d, ShouldResembleProto, &protocol.RealmData{})
   398  
   399  			// Fallback to root.
   400  			d, err = db.GetRealmData(c, "proj:unknown")
   401  			So(err, ShouldBeNil)
   402  			So(d, ShouldResembleProto, &protocol.RealmData{
   403  				EnforceInService: []string{"root"},
   404  			})
   405  
   406  			// Completely unknown.
   407  			d, err = db.GetRealmData(c, "unknown:unknown")
   408  			So(err, ShouldBeNil)
   409  			So(d, ShouldBeNil)
   410  		})
   411  	})
   412  
   413  	Convey("GetCertificates works", t, func(c C) {
   414  		tokenService := signingtest.NewSigner(&signing.ServiceInfo{
   415  			AppID:              "token-server",
   416  			ServiceAccountName: "token-server-account@example.com",
   417  		})
   418  
   419  		calls := 0
   420  
   421  		ctx := context.Background()
   422  		ctx = caching.WithEmptyProcessCache(ctx)
   423  
   424  		ctx = internal.WithTestTransport(ctx, func(r *http.Request, body string) (int, string) {
   425  			calls++
   426  			if r.URL.String() != "http://token-server/auth/api/v1/server/certificates" {
   427  				return 404, "Wrong URL"
   428  			}
   429  			certs, err := tokenService.Certificates(ctx)
   430  			if err != nil {
   431  				panic(err)
   432  			}
   433  			blob, err := json.Marshal(certs)
   434  			if err != nil {
   435  				panic(err)
   436  			}
   437  			return 200, string(blob)
   438  		})
   439  
   440  		certs, err := db.GetCertificates(ctx, "user:token-server-account@example.com")
   441  		So(err, ShouldBeNil)
   442  		So(certs, ShouldNotBeNil)
   443  
   444  		// Fetched one bundle.
   445  		So(calls, ShouldEqual, 1)
   446  
   447  		// For unknown signer returns (nil, nil).
   448  		certs, err = db.GetCertificates(ctx, "user:unknown@example.com")
   449  		So(err, ShouldBeNil)
   450  		So(certs, ShouldBeNil)
   451  	})
   452  
   453  	Convey("IsAllowedIP works", t, func() {
   454  		l, err := db.GetAllowlistForIdentity(c, "user:abc@example.com")
   455  		So(err, ShouldBeNil)
   456  		So(l, ShouldEqual, "allowlist")
   457  
   458  		l, err = db.GetAllowlistForIdentity(c, "user:unknown@example.com")
   459  		So(err, ShouldBeNil)
   460  		So(l, ShouldEqual, "")
   461  
   462  		call := func(ip, allowlist string) bool {
   463  			ipaddr := net.ParseIP(ip)
   464  			So(ipaddr, ShouldNotBeNil)
   465  			res, err := db.IsAllowedIP(c, ipaddr, allowlist)
   466  			So(err, ShouldBeNil)
   467  			return res
   468  		}
   469  
   470  		So(call("1.2.3.4", "allowlist"), ShouldBeTrue)
   471  		So(call("10.255.255.255", "allowlist"), ShouldBeTrue)
   472  		So(call("9.255.255.255", "allowlist"), ShouldBeFalse)
   473  		So(call("1.2.3.4", "empty"), ShouldBeFalse)
   474  	})
   475  
   476  	Convey("Revision works", t, func() {
   477  		So(Revision(&SnapshotDB{Rev: 123}), ShouldEqual, 123)
   478  		So(Revision(ErroringDB{}), ShouldEqual, 0)
   479  		So(Revision(nil), ShouldEqual, 0)
   480  	})
   481  
   482  	Convey("SnapshotDBFromTextProto works", t, func() {
   483  		db, err := SnapshotDBFromTextProto(strings.NewReader(`
   484  			groups {
   485  				name: "group"
   486  				members: "user:a@example.com"
   487  			}
   488  		`))
   489  		So(err, ShouldBeNil)
   490  		yes, err := db.IsMember(c, "user:a@example.com", []string{"group"})
   491  		So(err, ShouldBeNil)
   492  		So(yes, ShouldBeTrue)
   493  	})
   494  
   495  	Convey("SnapshotDBFromTextProto bad proto", t, func() {
   496  		_, err := SnapshotDBFromTextProto(strings.NewReader(`
   497  			groupz {}
   498  		`))
   499  		So(err, ShouldErrLike, "not a valid AuthDB text proto file")
   500  	})
   501  
   502  	Convey("SnapshotDBFromTextProto bad structure", t, func() {
   503  		_, err := SnapshotDBFromTextProto(strings.NewReader(`
   504  			groups {
   505  				name: "group 1"
   506  				nested: "group 2"
   507  			}
   508  			groups {
   509  				name: "group 2"
   510  				nested: "group 1"
   511  			}
   512  		`))
   513  		So(err, ShouldErrLike, "dependency cycle found")
   514  	})
   515  }
   516  
   517  var authDBPath = flag.String("authdb", "", "path to AuthDB proto to use in tests")
   518  var testAuthDB *protocol.AuthDB
   519  
   520  func TestMain(m *testing.M) {
   521  	flag.Parse()
   522  	if *authDBPath != "" {
   523  		testAuthDB = readTestDB(*authDBPath)
   524  	}
   525  	os.Exit(m.Run())
   526  }
   527  
   528  var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
   529  
   530  func randString(min, max int, seen stringset.Set) string {
   531  	for {
   532  		b := make([]rune, min+rand.Intn(max))
   533  		for i := range b {
   534  			b[i] = letterRunes[rand.Intn(len(letterRunes))]
   535  		}
   536  		s := string(b)
   537  		if seen.Add(s) {
   538  			return s
   539  		}
   540  	}
   541  }
   542  
   543  func readTestDB(path string) *protocol.AuthDB {
   544  	blob, err := os.ReadFile(path)
   545  	if err != nil {
   546  		panic(err)
   547  	}
   548  	authdb := protocol.AuthDB{}
   549  	if err := proto.Unmarshal(blob, &authdb); err != nil {
   550  		panic(err)
   551  	}
   552  	return &authdb
   553  }
   554  
   555  func makeTestDB(users, groups int) *protocol.AuthDB {
   556  	if testAuthDB != nil {
   557  		return testAuthDB
   558  	}
   559  
   560  	db := &protocol.AuthDB{}
   561  
   562  	domains := make([]string, 50)
   563  	seenDomains := stringset.New(50)
   564  	for i := range domains {
   565  		domains[i] = "@" + randString(5, 15, seenDomains) + ".com"
   566  	}
   567  	members := make([]string, users)
   568  	seenMembers := stringset.New(users)
   569  	for i := range members {
   570  		members[i] = "user:" + randString(3, 20, seenMembers) + domains[rand.Intn(len(domains))]
   571  	}
   572  
   573  	seenGroups := stringset.New(groups)
   574  	for i := 0; i < groups; i++ {
   575  		s := rand.Intn(len(members))
   576  		l := rand.Intn(len(members) - s)
   577  		db.Groups = append(db.Groups, &protocol.AuthGroup{
   578  			Name:    randString(10, 30, seenGroups),
   579  			Members: members[s : s+l],
   580  		})
   581  	}
   582  
   583  	return db
   584  }
   585  
   586  type queryableGraph interface {
   587  	IsMember(ident identity.Identity, group string) graph.IsMemberResult
   588  }
   589  
   590  func oldQueryableGraph(db *protocol.AuthDB) queryableGraph {
   591  	q, err := legacy.BuildGroups(db.Groups)
   592  	if err != nil {
   593  		panic(err)
   594  	}
   595  	return q
   596  }
   597  
   598  func newQueryableGraph(db *protocol.AuthDB) queryableGraph {
   599  	q, err := graph.BuildQueryable(db.Groups)
   600  	if err != nil {
   601  		panic(err)
   602  	}
   603  	return q
   604  }
   605  
   606  func memUsage(t *testing.T, cb func(*runtime.MemStats)) {
   607  	var m1, m2 runtime.MemStats
   608  
   609  	cb(&m1)
   610  	runtime.GC()
   611  	runtime.ReadMemStats(&m2)
   612  
   613  	t.Logf("HeapAlloc: %1.f Kb", float64(m1.HeapAlloc-m2.HeapAlloc)/1024)
   614  }
   615  
   616  func runMemUsageTest(t *testing.T, cb func(db *protocol.AuthDB) queryableGraph) {
   617  	db := makeTestDB(1000, 500)
   618  	memUsage(t, func(m *runtime.MemStats) {
   619  		q := cb(db)
   620  		runtime.GC()
   621  		runtime.ReadMemStats(m)
   622  		runtime.KeepAlive(q)
   623  	})
   624  }
   625  
   626  // Note: to run some benchmarks with real AuthDB:
   627  //   go run go.chromium.org/luci/auth/client/cmd/authdb-dump -output-proto-file auth.db
   628  //   go test . -run=TestMemUsage* -v -authdb auth.db
   629  //   go test -bench=QueryRealms -run=XXX -authdb auth.db
   630  
   631  func TestMemUsageOld(t *testing.T) {
   632  	runMemUsageTest(t, oldQueryableGraph)
   633  }
   634  
   635  func TestMemUsageNew(t *testing.T) {
   636  	runMemUsageTest(t, newQueryableGraph)
   637  }
   638  
   639  func TestCompareNewAndOld(t *testing.T) {
   640  	db := makeTestDB(100, 50)
   641  	old := oldQueryableGraph(db)
   642  	new := newQueryableGraph(db)
   643  
   644  	idSet := stringset.New(0)
   645  	for _, g := range db.Groups {
   646  		for _, m := range g.Members {
   647  			idSet.Add(m)
   648  		}
   649  	}
   650  	idents := idSet.ToSlice()
   651  	sort.Strings(idents)
   652  
   653  	for _, g := range db.Groups {
   654  		for _, id := range idents {
   655  			r1 := old.IsMember(identity.Identity(id), g.Name)
   656  			r2 := new.IsMember(identity.Identity(id), g.Name)
   657  			if r1 != r2 {
   658  				t.Fatalf("IsMember(%q, %q): %v != %v", id, g.Name, r1, r2)
   659  			}
   660  		}
   661  	}
   662  }
   663  
   664  func runIsMemberBenchmark(b *testing.B, db *protocol.AuthDB, q queryableGraph) {
   665  	idSet := stringset.New(0)
   666  	for _, g := range db.Groups {
   667  		for _, m := range g.Members {
   668  			idSet.Add(m)
   669  		}
   670  	}
   671  	idents := idSet.ToSlice()
   672  
   673  	b.ResetTimer()
   674  	for i := 0; i < b.N; i++ {
   675  		for _, g := range db.Groups {
   676  			for _, id := range idents {
   677  				q.IsMember(identity.Identity(id), g.Name)
   678  			}
   679  		}
   680  	}
   681  }
   682  
   683  func BenchmarkIsMemberOld(b *testing.B) {
   684  	db := makeTestDB(100, 50)
   685  	runIsMemberBenchmark(b, db, oldQueryableGraph(db))
   686  }
   687  
   688  func BenchmarkIsMemberNew(b *testing.B) {
   689  	db := makeTestDB(100, 50)
   690  	runIsMemberBenchmark(b, db, newQueryableGraph(db))
   691  }
   692  
   693  func BenchmarkQueryRealms(b *testing.B) {
   694  	ctx := context.Background()
   695  	db, err := NewSnapshotDB(makeTestDB(100, 50), "http://auth-service", 1234, false)
   696  	if err != nil {
   697  		b.Fatalf("bad AuthDB: %s", err)
   698  	}
   699  	b.ResetTimer()
   700  	for i := 0; i < b.N; i++ {
   701  		realms, err := db.QueryRealms(ctx, "user:someone@example.com", bbBuildsGet, "", nil)
   702  		if i == 0 {
   703  			if err != nil {
   704  				b.Fatalf("QueryRealms failed: %s", err)
   705  			}
   706  			b.Logf("Hits: %d", len(realms))
   707  		}
   708  	}
   709  }