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 }