go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/internal/realmsinternals/config.go (about)

     1  // Copyright 2023 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //	http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package realmsinternals
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/base64"
    21  	"encoding/gob"
    22  	"fmt"
    23  	"math"
    24  	"sort"
    25  	"sync"
    26  	"time"
    27  
    28  	"golang.org/x/sync/errgroup"
    29  	"google.golang.org/protobuf/encoding/prototext"
    30  
    31  	"go.chromium.org/luci/common/data/rand/mathrand"
    32  	"go.chromium.org/luci/common/data/sortby"
    33  	"go.chromium.org/luci/common/data/stringset"
    34  	"go.chromium.org/luci/common/errors"
    35  	"go.chromium.org/luci/common/logging"
    36  	realmsconf "go.chromium.org/luci/common/proto/realms"
    37  	"go.chromium.org/luci/config"
    38  	"go.chromium.org/luci/config/cfgclient"
    39  	"go.chromium.org/luci/gae/service/datastore"
    40  	"go.chromium.org/luci/gae/service/info"
    41  	"go.chromium.org/luci/server/auth/realms"
    42  	"go.chromium.org/luci/server/auth/service/protocol"
    43  
    44  	"go.chromium.org/luci/auth_service/impl/model"
    45  	"go.chromium.org/luci/auth_service/internal/permissions"
    46  )
    47  
    48  const (
    49  	// The services associated with Auth Service aka Chrome Infra Auth,
    50  	// to get its own configs.
    51  	Cria    = "services/chrome-infra-auth"
    52  	CriaDev = "services/chrome-infra-auth-dev"
    53  
    54  	// The AppID of the deployed development environment, so the correct
    55  	// config path will be used.
    56  	DevAppID = "chrome-infra-auth-dev"
    57  
    58  	// Paths to use within a project or service's folder when looking
    59  	// for realms configs.
    60  	RealmsCfgPath    = "realms.cfg"
    61  	RealmsDevCfgPath = "realms-dev.cfg"
    62  )
    63  
    64  // The maximum number of AuthDB revisions to produce when permissions
    65  // change and realms need to be reevaluated.
    66  const maxReevaluationRevisions int = 10
    67  
    68  type realmsMap struct {
    69  	mu     *sync.Mutex
    70  	cfgMap map[string]*config.Config
    71  }
    72  
    73  // CheckConfigChanges returns a slice of parameterless callbacks to
    74  // update the AuthDB based on detected realms.cfg and permissions
    75  // changes.
    76  //
    77  // Args:
    78  //   - permissionsDB: the current permissions and roles;
    79  //   - latest: RealmsCfgRev's for the realms configs fetched from
    80  //     LUCI Config;
    81  //   - stored: RealmsCfgRev's for the last processed realms configs;
    82  //   - dryRun: whether this is a dry run (if yes, changes wil not be
    83  //     committed in the AuthDB);
    84  //   - historicalComment: the comment to use in entities' history if
    85  //     changes are committed.
    86  //
    87  // Returns:
    88  //   - jobs: parameterless callbacks to update the AuthDB.
    89  func CheckConfigChanges(
    90  	ctx context.Context, permissionsDB *permissions.PermissionsDB,
    91  	latest []*model.RealmsCfgRev, stored []*model.RealmsCfgRev,
    92  	dryRun bool, historicalComment string) ([]func() error, error) {
    93  	toMap := func(revisions []*model.RealmsCfgRev) (map[string]*model.RealmsCfgRev, error) {
    94  		result := make(map[string]*model.RealmsCfgRev, len(revisions))
    95  		for _, cfgRev := range revisions {
    96  			result[cfgRev.ProjectID] = cfgRev
    97  		}
    98  
    99  		if len(result) != len(revisions) {
   100  			return nil, fmt.Errorf("multiple realms configs for the same project ID")
   101  		}
   102  		return result, nil
   103  	}
   104  
   105  	latestMap, err := toMap(latest)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  	storedMap, err := toMap(stored)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	var jobs []func() error
   115  
   116  	// For the realms configs that should be reevaluated, because they
   117  	// were generated with a previous revision of permissions.
   118  	toReevaluate := []*model.RealmsCfgRev{}
   119  
   120  	// Detect changes to realms configs. Going through the latest
   121  	// configs in a random order helps to progress if one of the configs
   122  	// is somehow very problematic (e.g. causes OOM). When the cron job
   123  	// is repeatedly retried, all healthy configs will eventually be
   124  	// processed before the problematic ones.
   125  	randomOrder := mathrand.Perm(ctx, len(latest))
   126  	for _, i := range randomOrder {
   127  		latestCfgRev := latest[i]
   128  		storedCfgRev, ok := storedMap[latestCfgRev.ProjectID]
   129  		if !ok || (storedCfgRev.ConfigDigest != latestCfgRev.ConfigDigest) {
   130  			// Add a job to update this project's realms.
   131  			revs := []*model.RealmsCfgRev{latestCfgRev}
   132  			comment := fmt.Sprintf("%s - using realms config rev %s", historicalComment, latestCfgRev.ConfigRev)
   133  			jobs = append(jobs, func() error {
   134  				return UpdateRealms(ctx, permissionsDB, revs, dryRun, comment)
   135  			})
   136  		} else if storedCfgRev.PermsRev != permissionsDB.Rev {
   137  			// This config needs to be reevaluated.
   138  			toReevaluate = append(toReevaluate, latestCfgRev)
   139  		}
   140  	}
   141  
   142  	// Detect realms.cfg that were removed completely.
   143  	for _, storedCfgRev := range stored {
   144  		if _, ok := latestMap[storedCfgRev.ProjectID]; !ok {
   145  			// Add a job to delete this project's realms.
   146  			projID := storedCfgRev.ProjectID
   147  			comment := fmt.Sprintf("%s - config no longer exists", historicalComment)
   148  			jobs = append(jobs, func() error {
   149  				return DeleteRealms(ctx, projID, dryRun, comment)
   150  			})
   151  		}
   152  	}
   153  
   154  	// Changing the permissions (e.g. adding a new permission to a widely used
   155  	// role) may affect ALL projects. In this case, generating a ton of AuthDB
   156  	// revisions is wasteful. We could try to generate a single giant revision,
   157  	// but it may end up being too big, hitting datastore limits. So we
   158  	// "heuristically" split it into at most maxReevaluationRevisions, hoping
   159  	// for the best.
   160  	reevaluations := len(toReevaluate)
   161  	batchSize := reevaluations / maxReevaluationRevisions
   162  	if batchSize < 1 {
   163  		batchSize = 1
   164  	}
   165  	for i := 0; i < reevaluations; i = i + batchSize {
   166  		revs := toReevaluate[i : i+batchSize]
   167  		comment := fmt.Sprintf("%s - generating realms with permissions rev %s",
   168  			historicalComment, permissionsDB.Rev)
   169  		jobs = append(jobs, func() error {
   170  			return UpdateRealms(ctx, permissionsDB, revs, dryRun, comment)
   171  		})
   172  	}
   173  
   174  	return jobs, nil
   175  }
   176  
   177  // UpdateRealms updates realms for projects given the fetched or previously processed realms.cfg.
   178  //
   179  // Returns
   180  //
   181  //	Annotated Error
   182  //		Unmarshalling proto error
   183  //		Failed Realm Expansion
   184  //		Failed to update datastore with Realms changes
   185  func UpdateRealms(ctx context.Context, db *permissions.PermissionsDB, revs []*model.RealmsCfgRev, dryRun bool, historicalComment string) error {
   186  	expanded := []*model.ExpandedRealms{}
   187  	for _, r := range revs {
   188  		logging.Infof(ctx, "expanding realms of project \"%s\"...", r.ProjectID)
   189  		start := time.Now()
   190  
   191  		parsed := &realmsconf.RealmsCfg{}
   192  		if err := prototext.Unmarshal(r.ConfigBody, parsed); err != nil {
   193  			return errors.Annotate(err, "couldn't unmarshal config body").Err()
   194  		}
   195  		expandedRev, err := ExpandRealms(db, r.ProjectID, parsed)
   196  		if err != nil {
   197  			return errors.Annotate(err, "failed to process realms of \"%s\"", r.ProjectID).Err()
   198  		}
   199  		expanded = append(expanded, &model.ExpandedRealms{
   200  			CfgRev: r,
   201  			Realms: expandedRev,
   202  		})
   203  
   204  		dt := time.Since(start)
   205  
   206  		if dt.Seconds() > 5.0 {
   207  			logging.Errorf(ctx, "realms expansion of \"%s\" is slow: %1.f", r.ProjectID, dt)
   208  		}
   209  	}
   210  	if len(expanded) == 0 {
   211  		return nil
   212  	}
   213  
   214  	logging.Infof(ctx, "entering transaction")
   215  	if err := model.UpdateAuthProjectRealms(ctx, expanded, db.Rev, dryRun, historicalComment); err != nil {
   216  		return err
   217  	}
   218  	logging.Infof(ctx, "transaction landed")
   219  	return nil
   220  }
   221  
   222  // DeleteRealms will try to delete the AuthProjectRealms for a given projectID.
   223  func DeleteRealms(ctx context.Context, projectID string, dryRun bool, historicalComment string) error {
   224  	switch err := model.DeleteAuthProjectRealms(ctx, projectID, dryRun, historicalComment); {
   225  	case errors.Is(err, datastore.ErrNoSuchEntity):
   226  		return errors.Annotate(err, "realms for %s do not exist or have already been deleted", projectID).Err()
   227  	case err != nil:
   228  		return err
   229  	default:
   230  		logging.Infof(ctx, "deleted realms for %s", projectID)
   231  		return nil
   232  	}
   233  }
   234  
   235  // ExpandRealms expands a realmsconf.RealmsCfg into a flat protocol.Realms.
   236  //
   237  // The returned protocol.Realms contains realms and permissions of a single
   238  // project only. Permissions not mentioned in the project's realms are omitted.
   239  // All protocol.Permission messages have names only (no metadata). api_version field
   240  // is omitted.
   241  //
   242  // All such protocol.Realms messages across all projects (plus a list of all
   243  // defined permissions with all their metadata) are later merged together into
   244  // a final universal protocol.Realms by merge() in the replication phase.
   245  func ExpandRealms(db *permissions.PermissionsDB, projectID string, realmsCfg *realmsconf.RealmsCfg) (*protocol.Realms, error) {
   246  	// internal is True when expanding internal realms (defined in a service
   247  	// config file). Such realms can use internal roles and permissions and
   248  	// they do not have implicit root bindings (since they are not associated
   249  	// with any "project:<X>" identity used in implicit root bindings).
   250  	internal := projectID == realms.InternalProject
   251  
   252  	// TODO(cjacomet): Add extra validation step to ensure code hasn't changed
   253  
   254  	// Make sure @root realm exists and append implicit bindings to it. We need
   255  	// to do this before enumerating the conditions below to actually instantiate
   256  	// all Condition objects that we'll need to visit (some of them may come from
   257  	// implicit bindings). Pre-instantiating them is important because we rely on
   258  	// their pointer address as map keys for lookups.
   259  	bindings := []*realmsconf.Binding{}
   260  	if !internal {
   261  		bindings = db.ImplicitRootBindings(projectID)
   262  	}
   263  	realmsMap := toRealmsMap(realmsCfg, bindings)
   264  
   265  	// We will need to visit realms in sorted order twice. Sort once and remember.
   266  	realmsList := make([]*realmsconf.Realm, 0, len(realmsMap))
   267  	for _, v := range realmsMap {
   268  		realmsList = append(realmsList, v)
   269  	}
   270  	sort.Slice(realmsList, func(i, j int) bool {
   271  		return realmsList[i].GetName() < realmsList[j].GetName()
   272  	})
   273  
   274  	customRolesMap := make(map[string]*realmsconf.CustomRole, len(realmsCfg.GetCustomRoles()))
   275  	for _, r := range realmsCfg.GetCustomRoles() {
   276  		customRolesMap[r.GetName()] = r
   277  	}
   278  
   279  	condsSet := &ConditionsSet{
   280  		indexMapping: make(map[*realmsconf.Condition]uint32),
   281  		normalized:   make(map[string]*conditionMapTuple),
   282  	}
   283  
   284  	// Prepopulate condsSet with all conditions mentioned in all bindings to
   285  	// normalize, dedup and map them to integers. Integers are faster to work with
   286  	// and we'll need them for the final proto message.
   287  	for _, realm := range realmsList {
   288  		for _, binding := range realm.Bindings {
   289  			for _, cond := range binding.Conditions {
   290  				if err := condsSet.addCond(cond); err != nil {
   291  					return nil, err
   292  				}
   293  			}
   294  		}
   295  	}
   296  
   297  	allConditions := condsSet.finalize()
   298  
   299  	rolesExpander := &RolesExpander{
   300  		builtinRoles: db.Roles,
   301  		customRoles:  customRolesMap,
   302  		permissions:  map[string]uint32{},
   303  		roles:        map[string]*indexSet{},
   304  	}
   305  
   306  	realmsExpander := &RealmsExpander{
   307  		rolesExpander: rolesExpander,
   308  		condsSet:      condsSet,
   309  		realms:        realmsMap,
   310  		data:          map[string]*protocol.RealmData{},
   311  	}
   312  
   313  	type realmMappingObj struct {
   314  		name      string
   315  		permTuple map[string]stringset.Set
   316  	}
   317  
   318  	realmsToReturn := []*realmMappingObj{}
   319  	var permsToPrincipal map[string]stringset.Set
   320  
   321  	// Visit all realms and build preliminary bindings as pairs of
   322  	// (permission indexes, a list of principals who have them). The
   323  	// bindings are preliminary since we don't know final permission indexes yet
   324  	// and instead use some internal indexes as generated by RolesExpander. We need
   325  	// to finish this first pass to gather the list of ALL used permissions, so we
   326  	// can calculate final indexes. This is done inside of rolesExpander.
   327  	for _, cfgRealm := range realmsList {
   328  		// Build a mapping from a principal + conditions to the permissions set.
   329  		//
   330  		// Each map entry ---- means principal is granted the given set of permissions
   331  		// if all given conditions allow it.
   332  		//
   333  		// This step essentially deduplicates permission bindings that result from
   334  		// expanding realms and role inheritance chains.
   335  		principalToPerms := map[string]*indexSet{}
   336  		principalBindings, err := realmsExpander.perPrincipalBindings(cfgRealm.GetName())
   337  		if err != nil {
   338  			return nil, err
   339  		}
   340  		for _, principal := range principalBindings {
   341  			key := toKey(principalPerms{Principal: principal.name, Conds: principal.conditions})
   342  			if _, ok := principalToPerms[key]; !ok {
   343  				principalToPerms[key] = emptyIndexSet()
   344  			}
   345  			principalToPerms[key].update(principal.permissions)
   346  		}
   347  
   348  		// Combine entries with the same set of permissions + conditions into one.
   349  		//
   350  		// Each map entry ---- means all principals are granted all given permissions
   351  		// if all given conditions allow it.
   352  		//
   353  		// This step merges principal sets of identical bindings to have a more compact
   354  		// final representation.
   355  		permsToPrincipal = map[string]stringset.Set{}
   356  		for key, perms := range principalToPerms {
   357  			principalToPermsObj := toEntry(key)
   358  			permsNorm := perms.toSortedSlice()
   359  			permsToPrincipalObj := principalPerms{
   360  				Conds: principalToPermsObj.Conds,
   361  				Perms: permsNorm,
   362  			}
   363  			key := toKey(permsToPrincipalObj)
   364  			if permsToPrincipal[key] == nil {
   365  				permsToPrincipal[key] = stringset.Set{}
   366  			}
   367  			permsToPrincipal[key].Add(principalToPermsObj.Principal)
   368  		}
   369  		realmsToReturn = append(realmsToReturn, &realmMappingObj{cfgRealm.GetName(), permsToPrincipal})
   370  	}
   371  
   372  	perms, indexMap := rolesExpander.sortedPermissions()
   373  
   374  	permsSorted := make([]*protocol.Permission, 0, len(perms))
   375  	for _, p := range perms {
   376  		permsSorted = append(permsSorted, &protocol.Permission{
   377  			Name:     p,
   378  			Internal: internal,
   379  		})
   380  	}
   381  
   382  	realmsReturned := make([]*protocol.Realm, 0, len(realmsToReturn))
   383  	for _, r := range realmsToReturn {
   384  		data, err := realmsExpander.realmData(r.name, []*protocol.RealmData{})
   385  		if err != nil {
   386  			return nil, errors.Annotate(err, "couldn't fetch realm data").Err()
   387  		}
   388  		realmsReturned = append(realmsReturned, &protocol.Realm{
   389  			Name:     fmt.Sprintf("%s:%s", projectID, r.name),
   390  			Bindings: toNormalizedBindings(r.permTuple, indexMap),
   391  			Data:     data,
   392  		})
   393  	}
   394  
   395  	return &protocol.Realms{
   396  		Permissions: permsSorted,
   397  		Conditions:  allConditions,
   398  		Realms:      realmsReturned,
   399  	}, nil
   400  }
   401  
   402  // principalPerms is a wrapper struct to represent a relationship
   403  // between a principal and permissions + conditions. The encoded
   404  // form of this struct is used as a key to deduplicate.
   405  type principalPerms struct {
   406  	Principal string
   407  	Conds     []uint32
   408  	Perms     []uint32
   409  }
   410  
   411  // toKey converts a principalPerms struct to a key.
   412  // this is useful for deduplicating principal to permissions
   413  // bindings.
   414  func toKey(p principalPerms) string {
   415  	b := bytes.Buffer{}
   416  	e := gob.NewEncoder(&b)
   417  	err := e.Encode(p)
   418  	if err != nil {
   419  		fmt.Println(`failed gob Encode`, err)
   420  	}
   421  	return base64.StdEncoding.EncodeToString(b.Bytes())
   422  }
   423  
   424  // toEntry converts the key to an equivalent principalPerms
   425  // struct.
   426  func toEntry(key string) principalPerms {
   427  	m := principalPerms{}
   428  	by, err := base64.StdEncoding.DecodeString(key)
   429  	if err != nil {
   430  		fmt.Println(`failed base64 Decode`, err)
   431  	}
   432  	b := bytes.Buffer{}
   433  	b.Write(by)
   434  	d := gob.NewDecoder(&b)
   435  	err = d.Decode(&m)
   436  	if err != nil {
   437  		fmt.Println(`failed gob Decode`, err)
   438  	}
   439  	return m
   440  }
   441  
   442  func toRealmsMap(realmsCfg *realmsconf.RealmsCfg, implicitRootBindings []*realmsconf.Binding) map[string]*realmsconf.Realm {
   443  	realmsMap := map[string]*realmsconf.Realm{}
   444  	for _, r := range realmsCfg.GetRealms() {
   445  		realmsMap[r.GetName()] = r
   446  	}
   447  	root := &realmsconf.Realm{Name: realms.RootRealm}
   448  	if res, ok := realmsMap[realms.RootRealm]; ok {
   449  		root = res
   450  	}
   451  	root.Bindings = append(root.Bindings, implicitRootBindings...)
   452  	realmsMap[realms.RootRealm] = root
   453  	return realmsMap
   454  }
   455  
   456  type normalizedStruct struct {
   457  	permsSorted []uint32
   458  	conds       []uint32
   459  	princ       []string
   460  }
   461  
   462  // toNormalizedBindings produces a sorted slice of *protocol.Binding.
   463  //
   464  // Bindings are given as a map from principalPerms -> list of principles
   465  // that should have all given permission if all given conditions allow. In
   466  // the principalPerms only the permissions and conditions are filled.
   467  //
   468  // Conditions are specified as indexes in ConditionSet, we use them as they are,
   469  // since by consruction of ConditionsSet all conditions are in use and we don't
   470  // need any extra filtering (and consequently index remapping to skip gaps) as we
   471  // do for permissions.
   472  //
   473  // permsToPrincipal is a map mapping {Conds, Perms} -> principals.
   474  // indexMapping defines how to remap permission indexes (old -> new).
   475  func toNormalizedBindings(permsToPrincipal map[string]stringset.Set, indexMapping []uint32) []*protocol.Binding {
   476  	normalized := []*normalizedStruct{}
   477  
   478  	for key, principals := range permsToPrincipal {
   479  		permsConds := toEntry(key)
   480  		principalsCopy := principals.ToSortedSlice()
   481  
   482  		idxSet := emptyIndexSet()
   483  		for _, oldPermIdx := range permsConds.Perms {
   484  			idxSet.add(indexMapping[oldPermIdx])
   485  		}
   486  		normalized = append(normalized, &normalizedStruct{
   487  			permsSorted: idxSet.toSortedSlice(),
   488  			conds:       permsConds.Conds,
   489  			princ:       principalsCopy,
   490  		})
   491  	}
   492  	bindings := []*protocol.Binding{}
   493  
   494  	sort.Slice(normalized, sortby.Chain{
   495  		func(i, j int) bool { return sliceCompare(normalized[i].permsSorted, normalized[j].permsSorted) },
   496  		func(i, j int) bool { return sliceCompare(normalized[i].conds, normalized[j].conds) },
   497  		func(i, j int) bool { return sliceCompare(normalized[i].princ, normalized[j].princ) },
   498  	}.Use)
   499  
   500  	for _, k := range normalized {
   501  		bindings = append(bindings, &protocol.Binding{
   502  			Permissions: k.permsSorted,
   503  			Principals:  k.princ,
   504  			Conditions:  k.conds,
   505  		})
   506  	}
   507  
   508  	return bindings
   509  }
   510  
   511  func sliceCompare[T string | uint32](sli []T, slj []T) bool {
   512  	sliceLen := int(math.Min(float64(len(sli)), float64(len(slj))))
   513  	for idx := 0; idx < sliceLen; idx++ {
   514  		if sli[idx] != slj[idx] {
   515  			return sli[idx] < slj[idx]
   516  		}
   517  	}
   518  	return len(sli) < len(slj)
   519  }
   520  
   521  // GetConfigs fetches the configs concurrently; the
   522  // latest configs from luci-cfg, the stored config meta from datastore.
   523  //
   524  // Errors
   525  //
   526  //	ErrNoConfig -- config is not found
   527  //	annotated error -- for all other errors
   528  func GetConfigs(ctx context.Context) ([]*model.RealmsCfgRev, []*model.RealmsCfgRev, error) {
   529  	targetCfgPath := cfgPath(ctx)
   530  	projects, err := cfgclient.ProjectsWithConfig(ctx, targetCfgPath)
   531  	if err != nil {
   532  		return nil, nil, err
   533  	}
   534  	logging.Debugf(ctx, "%d projects with %s: %s", len(projects), targetCfgPath, projects)
   535  
   536  	// client to fetch configs
   537  	client := cfgclient.Client(ctx)
   538  	latestRevs := make([]*model.RealmsCfgRev, len(projects)+1)
   539  
   540  	eg, childCtx := errgroup.WithContext(ctx)
   541  
   542  	latestMap := realmsMap{
   543  		mu:     &sync.Mutex{},
   544  		cfgMap: make(map[string]*config.Config, len(projects)+1),
   545  	}
   546  
   547  	storedMeta := []*model.AuthProjectRealmsMeta{}
   548  
   549  	self := func(ctx context.Context) string {
   550  		if cfgPath(ctx) == RealmsDevCfgPath {
   551  			return CriaDev
   552  		}
   553  		return Cria
   554  	}
   555  
   556  	// Get Project Metadata configs stored in datastore
   557  	eg.Go(func() error {
   558  		storedMeta, err = model.GetAllAuthProjectRealmsMeta(ctx)
   559  		if err != nil {
   560  			return err
   561  		}
   562  		return nil
   563  	})
   564  
   565  	// Get self config i.e. services/chrome-infra-auth-dev/realms-dev.cfg
   566  	// or services/chrome-infra-auth/realms.cfg.
   567  	eg.Go(func() error {
   568  		return latestMap.getLatestConfig(childCtx, client, self(ctx))
   569  	})
   570  
   571  	// Get Project Configs
   572  	for _, project := range projects {
   573  		project := project
   574  		eg.Go(func() error {
   575  			return latestMap.getLatestConfig(childCtx, client, project)
   576  		})
   577  	}
   578  
   579  	err = eg.Wait()
   580  	if err != nil {
   581  		return nil, nil, err
   582  	}
   583  
   584  	// Log the projects that have stored AuthProjectRealmsMeta, to aid in
   585  	// debugging.
   586  	projectsWithMeta := make([]string, len(storedMeta))
   587  	for i, meta := range storedMeta {
   588  		metaProj, _ := meta.ProjectID()
   589  		projectsWithMeta[i] = metaProj
   590  	}
   591  	logging.Debugf(ctx, "fetched realms metadata for %d projects: %s", len(storedMeta), projectsWithMeta)
   592  
   593  	storedRevs := make([]*model.RealmsCfgRev, len(storedMeta))
   594  
   595  	idx := 0
   596  	for projID, cfg := range latestMap.cfgMap {
   597  		latestRevs[idx] = &model.RealmsCfgRev{
   598  			ProjectID:    projID,
   599  			ConfigRev:    cfg.Revision,
   600  			ConfigDigest: cfg.ContentHash,
   601  			ConfigBody:   []byte(cfg.Content),
   602  		}
   603  		idx++
   604  	}
   605  
   606  	for i, meta := range storedMeta {
   607  		projID, err := meta.ProjectID()
   608  		if err != nil {
   609  			return nil, nil, err
   610  		}
   611  		storedRevs[i] = &model.RealmsCfgRev{
   612  			ProjectID:    projID,
   613  			ConfigRev:    meta.ConfigRev,
   614  			ConfigDigest: meta.ConfigDigest,
   615  			PermsRev:     meta.PermsRev,
   616  		}
   617  	}
   618  
   619  	if err != nil {
   620  		return nil, nil, err
   621  	}
   622  
   623  	return latestRevs, storedRevs, nil
   624  }
   625  
   626  // getLatestConfig fetches the most up to date realms.cfg for a given project, unless
   627  // fetching the config for self, in which case it fetches the service config. The configs are
   628  // written to a map mapping K: project name (string) -> V: *config.Config.
   629  func (r *realmsMap) getLatestConfig(ctx context.Context, client config.Interface, project string) error {
   630  	project, cfgSet, err := r.cfgSet(project)
   631  	if err != nil {
   632  		return err
   633  	}
   634  
   635  	targetCfgPath := cfgPath(ctx)
   636  	cfg, err := client.GetConfig(ctx, cfgSet, targetCfgPath, false)
   637  	if err != nil {
   638  		return errors.Annotate(err, "failed to fetch %s for %s", targetCfgPath, project).Err()
   639  	}
   640  
   641  	r.mu.Lock()
   642  	r.cfgMap[project] = cfg
   643  	r.mu.Unlock()
   644  
   645  	return nil
   646  }
   647  
   648  // cfgPath is a helper function to know which cfg, depending on dev or prod env.
   649  func cfgPath(ctx context.Context) string {
   650  	if info.IsDevAppServer(ctx) || info.AppID(ctx) == DevAppID {
   651  		return RealmsDevCfgPath
   652  	}
   653  	return RealmsCfgPath
   654  }
   655  
   656  // cfgSet is a helper function to know which configSet to use, this is necessary for
   657  // getting the realms cfg for CrIA or CrIADev since the realms.cfg is stored as
   658  // a service config instead of a project config.
   659  func (r *realmsMap) cfgSet(project string) (string, config.Set, error) {
   660  	if project == Cria || project == CriaDev {
   661  		r.mu.Lock()
   662  		defer r.mu.Unlock()
   663  		if _, ok := r.cfgMap[realms.InternalProject]; ok {
   664  			return "", "", fmt.Errorf("unexpected LUCI Project: %s", realms.InternalProject)
   665  		}
   666  		return realms.InternalProject, config.Set(project), nil
   667  	}
   668  
   669  	ps, err := config.ProjectSet(project)
   670  	if err != nil {
   671  		return "", "", err
   672  	}
   673  	return project, ps, nil
   674  }