go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/acls/acls_test.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 acls 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 "strings" 22 "testing" 23 24 "google.golang.org/grpc/codes" 25 "google.golang.org/protobuf/encoding/prototext" 26 "google.golang.org/protobuf/proto" 27 28 "go.chromium.org/luci/auth/identity" 29 "go.chromium.org/luci/common/data/stringset" 30 "go.chromium.org/luci/config" 31 "go.chromium.org/luci/config/cfgclient" 32 cfgmem "go.chromium.org/luci/config/impl/memory" 33 "go.chromium.org/luci/gae/impl/memory" 34 "go.chromium.org/luci/server/auth/authtest" 35 "go.chromium.org/luci/server/auth/realms" 36 37 configpb "go.chromium.org/luci/swarming/proto/config" 38 "go.chromium.org/luci/swarming/server/cfg" 39 40 . "github.com/smartystreets/goconvey/convey" 41 . "go.chromium.org/luci/common/testing/assertions" 42 ) 43 44 func TestServerLevel(t *testing.T) { 45 t.Parallel() 46 47 ctx := context.Background() 48 49 cfg := mockedConfig(&configpb.AuthSettings{ 50 AdminsGroup: "admins", 51 BotBootstrapGroup: "bootstrap", 52 PrivilegedUsersGroup: "privileged", 53 ViewAllBotsGroup: "view-all-bots", 54 ViewAllTasksGroup: "view-all-tasks", 55 }, nil, nil) 56 57 db := authtest.NewFakeDB( 58 authtest.MockMembership("user:admin@example.com", "admins"), 59 authtest.MockMembership("user:bootstrap@example.com", "bootstrap"), 60 authtest.MockMembership("user:privileged@example.com", "privileged"), 61 authtest.MockMembership("user:view-all-bots@example.com", "view-all-bots"), 62 authtest.MockMembership("user:view-all-tasks@example.com", "view-all-tasks"), 63 ) 64 65 permittedPerms := func(caller identity.Identity) []realms.Permission { 66 chk := Checker{cfg: cfg, db: db, caller: caller} 67 var permitted []realms.Permission 68 for _, perm := range allPermissions() { 69 res := chk.CheckServerPerm(ctx, perm) 70 So(res.InternalError, ShouldBeFalse) 71 if res.Permitted { 72 permitted = append(permitted, perm) 73 } 74 } 75 return permitted 76 } 77 78 Convey("Unknown", t, func() { 79 So(permittedPerms("user:unknown@example.com"), ShouldBeEmpty) 80 }) 81 82 Convey("Admin", t, func() { 83 assertSame(permittedPerms("user:admin@example.com"), []realms.Permission{ 84 PermTasksGet, 85 PermTasksCancel, 86 PermPoolsListBots, 87 PermPoolsListTasks, 88 PermPoolsCreateBot, 89 PermPoolsDeleteBot, 90 PermPoolsTerminateBot, 91 PermPoolsCancelTask, 92 }) 93 }) 94 95 Convey("Bootstrap", t, func() { 96 assertSame(permittedPerms("user:bootstrap@example.com"), []realms.Permission{ 97 PermPoolsCreateBot, 98 }) 99 }) 100 101 Convey("Privileged", t, func() { 102 assertSame(permittedPerms("user:privileged@example.com"), []realms.Permission{ 103 PermTasksGet, 104 PermPoolsListBots, 105 PermPoolsListTasks, 106 }) 107 }) 108 109 Convey("View all bots", t, func() { 110 assertSame(permittedPerms("user:view-all-bots@example.com"), []realms.Permission{ 111 PermPoolsListBots, 112 }) 113 }) 114 115 Convey("View all tasks", t, func() { 116 assertSame(permittedPerms("user:view-all-tasks@example.com"), []realms.Permission{ 117 PermTasksGet, 118 PermPoolsListTasks, 119 }) 120 }) 121 122 Convey("Error message", t, func() { 123 chk := Checker{cfg: cfg, db: db, caller: "user:unknown@example.com"} 124 res := chk.CheckServerPerm(ctx, PermTasksCancel) 125 So(res.Permitted, ShouldBeFalse) 126 err := res.ToGrpcErr() 127 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 128 So(err, ShouldErrLike, `the caller "user:unknown@example.com" doesn't have server-level permission "swarming.tasks.cancel"`) 129 }) 130 } 131 132 func TestPoolLevel(t *testing.T) { 133 const ( 134 unknownID identity.Identity = "user:unknown@example.com" 135 privilegedID identity.Identity = "user:privileged@example.com" 136 authorizedID identity.Identity = "user:authorized@example.com" 137 anotherID identity.Identity = "user:another@example.com" 138 ) 139 140 t.Parallel() 141 142 ctx := context.Background() 143 144 allPools := []string{ 145 "visible-pool-1", 146 "visible-pool-2", 147 "hidden-pool-1", 148 "hidden-pool-2", 149 "deleted-pool-1", // has no realm association 150 "deleted-pool-2", // has no realm association 151 } 152 153 cfg := mockedConfig(&configpb.AuthSettings{ 154 PrivilegedUsersGroup: "privileged", 155 }, map[string]string{ 156 "visible-pool-1": "project:visible-realm", 157 "visible-pool-2": "project:visible-realm", 158 "hidden-pool-1": "project:hidden-realm", 159 "hidden-pool-2": "project:hidden-realm", 160 }, nil) 161 162 db := authtest.NewFakeDB( 163 authtest.MockMembership(privilegedID, "privileged"), 164 authtest.MockPermission(authorizedID, "project:visible-realm", PermPoolsListBots), 165 authtest.MockPermission(anotherID, "project:hidden-realm", PermPoolsListBots), 166 ) 167 168 Convey("CheckPoolPerm", t, func() { 169 // Note the implementation doesn't depend on exact permission being checked, 170 // we'll check only PermPoolsListBots. 171 poolsWithListBots := func(caller identity.Identity) []string { 172 chk := Checker{cfg: cfg, db: db, caller: caller} 173 var pools []string 174 for _, pool := range allPools { 175 res := chk.CheckPoolPerm(ctx, pool, PermPoolsListBots) 176 So(res.InternalError, ShouldBeFalse) 177 if res.Permitted { 178 pools = append(pools, pool) 179 } 180 } 181 return pools 182 } 183 184 Convey("Unknown", func() { 185 So(poolsWithListBots(unknownID), ShouldBeEmpty) 186 }) 187 188 Convey("Privileged", func() { 189 So(poolsWithListBots(privilegedID), ShouldResemble, allPools) 190 }) 191 192 Convey("Authorized", func() { 193 So(poolsWithListBots(authorizedID), ShouldResemble, []string{ 194 "visible-pool-1", 195 "visible-pool-2", 196 }) 197 }) 198 199 Convey("Error message", func() { 200 chk := Checker{cfg: cfg, db: db, caller: unknownID} 201 for _, pool := range allPools { 202 res := chk.CheckPoolPerm(ctx, pool, PermPoolsListBots) 203 So(res.Permitted, ShouldBeFalse) 204 err := res.ToGrpcErr() 205 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 206 So(err, ShouldErrLike, 207 fmt.Sprintf(`the caller "user:unknown@example.com" doesn't have permission "swarming.pools.listBots"`+ 208 ` in the pool %q or the pool doesn't exist`, pool)) 209 } 210 }) 211 }) 212 213 Convey("FilterPoolsByPerm", t, func() { 214 poolsWithListBots := func(caller identity.Identity) []string { 215 chk := Checker{cfg: cfg, db: db, caller: caller} 216 pools, err := chk.FilterPoolsByPerm(ctx, allPools, PermPoolsListBots) 217 So(err, ShouldBeNil) 218 return pools 219 } 220 221 Convey("Unknown", func() { 222 So(poolsWithListBots(unknownID), ShouldBeEmpty) 223 }) 224 225 Convey("Privileged", func() { 226 So(poolsWithListBots(privilegedID), ShouldResemble, allPools) 227 }) 228 229 Convey("Authorized", func() { 230 So(poolsWithListBots(authorizedID), ShouldResemble, []string{ 231 "visible-pool-1", 232 "visible-pool-2", 233 }) 234 }) 235 }) 236 237 Convey("CheckAllPoolsPerm", t, func() { 238 checkAll := func(caller identity.Identity, pools []string) bool { 239 chk := Checker{cfg: cfg, db: db, caller: caller} 240 res := chk.CheckAllPoolsPerm(ctx, pools, PermPoolsListBots) 241 So(res.InternalError, ShouldBeFalse) 242 return res.Permitted 243 } 244 245 Convey("Unknown", func() { 246 So(checkAll(unknownID, []string{"visible-pool-1"}), ShouldBeFalse) 247 So(checkAll(unknownID, []string{"visible-pool-1", "visible-pool-2"}), ShouldBeFalse) 248 So(checkAll(unknownID, allPools), ShouldBeFalse) 249 }) 250 251 Convey("Privileged", func() { 252 So(checkAll(privilegedID, []string{"visible-pool-1"}), ShouldBeTrue) 253 So(checkAll(privilegedID, []string{"visible-pool-1", "visible-pool-2"}), ShouldBeTrue) 254 So(checkAll(privilegedID, []string{"hidden-pool-1"}), ShouldBeTrue) 255 So(checkAll(privilegedID, []string{"hidden-pool-1", "hidden-pool-2"}), ShouldBeTrue) 256 So(checkAll(privilegedID, allPools), ShouldBeTrue) 257 }) 258 259 Convey("Authorized", func() { 260 So(checkAll(authorizedID, []string{"visible-pool-1"}), ShouldBeTrue) 261 So(checkAll(authorizedID, []string{"visible-pool-1", "visible-pool-2"}), ShouldBeTrue) 262 So(checkAll(authorizedID, []string{"hidden-pool-1"}), ShouldBeFalse) 263 So(checkAll(authorizedID, []string{"hidden-pool-1", "hidden-pool-2"}), ShouldBeFalse) 264 So(checkAll(authorizedID, []string{"visible-pool-1", "visible-pool-2", "hidden-pool-1"}), ShouldBeFalse) 265 So(checkAll(authorizedID, []string{"visible-pool-1", "visible-pool-2", "deleted-pool-1"}), ShouldBeFalse) 266 }) 267 268 Convey("Error message", func() { 269 chk := Checker{cfg: cfg, db: db, caller: authorizedID} 270 res := chk.CheckAllPoolsPerm(ctx, allPools, PermPoolsListBots) 271 So(res.InternalError, ShouldBeFalse) 272 err := res.ToGrpcErr() 273 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 274 So(err, ShouldErrLike, `the caller "user:authorized@example.com" doesn't have permission "swarming.pools.listBots" in some of the requested pools`) 275 }) 276 }) 277 278 Convey("CheckAnyPoolsPerm", t, func() { 279 checkAny := func(caller identity.Identity, pools []string) bool { 280 chk := Checker{cfg: cfg, db: db, caller: caller} 281 res := chk.CheckAnyPoolsPerm(ctx, pools, PermPoolsListBots) 282 So(res.InternalError, ShouldBeFalse) 283 return res.Permitted 284 } 285 286 Convey("Unknown", func() { 287 So(checkAny(unknownID, []string{"visible-pool-1"}), ShouldBeFalse) 288 So(checkAny(unknownID, []string{"visible-pool-1", "visible-pool-2"}), ShouldBeFalse) 289 So(checkAny(unknownID, allPools), ShouldBeFalse) 290 }) 291 292 Convey("Privileged", func() { 293 So(checkAny(privilegedID, []string{"visible-pool-1"}), ShouldBeTrue) 294 So(checkAny(privilegedID, []string{"visible-pool-1", "visible-pool-2"}), ShouldBeTrue) 295 So(checkAny(privilegedID, []string{"hidden-pool-1"}), ShouldBeTrue) 296 So(checkAny(privilegedID, []string{"hidden-pool-1", "hidden-pool-2"}), ShouldBeTrue) 297 So(checkAny(privilegedID, allPools), ShouldBeTrue) 298 }) 299 300 Convey("Authorized", func() { 301 So(checkAny(authorizedID, []string{"visible-pool-1"}), ShouldBeTrue) 302 So(checkAny(authorizedID, []string{"visible-pool-1", "visible-pool-2"}), ShouldBeTrue) 303 So(checkAny(authorizedID, []string{"hidden-pool-1"}), ShouldBeFalse) 304 So(checkAny(authorizedID, []string{"hidden-pool-1", "hidden-pool-2"}), ShouldBeFalse) 305 So(checkAny(authorizedID, []string{"deleted-pool-1"}), ShouldBeFalse) 306 So(checkAny(authorizedID, []string{"deleted-pool-1", "deleted-pool-2"}), ShouldBeFalse) 307 So(checkAny(authorizedID, []string{"hidden-pool-1", "visible-pool-1"}), ShouldBeTrue) 308 So(checkAny(authorizedID, []string{"deleted-pool-1", "visible-pool-1"}), ShouldBeTrue) 309 }) 310 311 Convey("Error message", func() { 312 chk := Checker{cfg: cfg, db: db, caller: authorizedID} 313 res := chk.CheckAnyPoolsPerm(ctx, []string{"hidden-pool-1", "deleted-pool-1"}, PermPoolsListBots) 314 So(res.InternalError, ShouldBeFalse) 315 err := res.ToGrpcErr() 316 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 317 So(err, ShouldErrLike, `the caller "user:authorized@example.com" doesn't have permission "swarming.pools.listBots" in any of the requested pools`) 318 }) 319 }) 320 } 321 322 func TestBotLevel(t *testing.T) { 323 const ( 324 unknownID identity.Identity = "user:unknown@example.com" 325 privilegedID identity.Identity = "user:privileged@example.com" 326 authorizedID identity.Identity = "user:authorized@example.com" 327 ) 328 329 t.Parallel() 330 331 ctx := context.Background() 332 333 cfg := mockedConfig(&configpb.AuthSettings{ 334 PrivilegedUsersGroup: "privileged", 335 }, map[string]string{ 336 "visible-pool": "project:visible-realm", 337 "hidden-pool": "project:hidden-realm", 338 }, map[string][]string{ 339 "visible-bot": {"visible-pool", "hidden-pool"}, 340 "hidden-bot": {"hidden-pool"}, 341 }) 342 343 db := authtest.NewFakeDB( 344 authtest.MockMembership(privilegedID, "privileged"), 345 authtest.MockPermission(authorizedID, "project:visible-realm", PermPoolsListBots), 346 ) 347 348 checkBotVisible := func(caller identity.Identity, botID string) bool { 349 chk := Checker{cfg: cfg, db: db, caller: caller} 350 res := chk.CheckBotPerm(ctx, botID, PermPoolsListBots) 351 So(res.InternalError, ShouldBeFalse) 352 return res.Permitted 353 } 354 355 Convey("Unknown", t, func() { 356 So(checkBotVisible(unknownID, "visible-bot"), ShouldBeFalse) 357 So(checkBotVisible(unknownID, "hidden-bot"), ShouldBeFalse) 358 So(checkBotVisible(unknownID, "unknown-bot"), ShouldBeFalse) 359 }) 360 361 Convey("Privileged", t, func() { 362 So(checkBotVisible(privilegedID, "visible-bot"), ShouldBeTrue) 363 So(checkBotVisible(privilegedID, "hidden-bot"), ShouldBeTrue) 364 So(checkBotVisible(privilegedID, "unknown-bot"), ShouldBeTrue) 365 }) 366 367 Convey("Authorized", t, func() { 368 So(checkBotVisible(authorizedID, "visible-bot"), ShouldBeTrue) 369 So(checkBotVisible(authorizedID, "hidden-bot"), ShouldBeFalse) 370 So(checkBotVisible(authorizedID, "unknown-bot"), ShouldBeFalse) 371 }) 372 373 Convey("Error message", t, func() { 374 chk := Checker{cfg: cfg, db: db, caller: authorizedID} 375 res := chk.CheckBotPerm(ctx, "hidden-bot", PermPoolsListBots) 376 So(res.InternalError, ShouldBeFalse) 377 err := res.ToGrpcErr() 378 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 379 So(err, ShouldErrLike, `the caller "user:authorized@example.com" doesn't have permission `+ 380 `"swarming.pools.listBots" in the pool that contains bot "hidden-bot" or this bot doesn't exist`) 381 }) 382 } 383 384 func TestTaskLevel(t *testing.T) { 385 const ( 386 unknownID identity.Identity = "user:unknown@example.com" 387 adminID identity.Identity = "user:admin@example.com" 388 authorizedID identity.Identity = "user:authorized@example.com" 389 ) 390 391 t.Parallel() 392 393 ctx := context.Background() 394 395 cfg := mockedConfig(&configpb.AuthSettings{ 396 AdminsGroup: "admins", 397 }, map[string]string{ 398 "visible-pool": "project:visible-pool-realm", 399 "hidden-pool": "project:hidden-pool-realm", 400 }, map[string][]string{ 401 "visible-bot": {"visible-pool", "hidden-pool"}, 402 "hidden-bot": {"hidden-pool"}, 403 }) 404 405 db := authtest.NewFakeDB( 406 authtest.MockMembership(adminID, "admins"), 407 authtest.MockPermission(authorizedID, "project:visible-task-realm", PermTasksCancel), 408 authtest.MockPermission(authorizedID, "project:visible-pool-realm", PermPoolsCancelTask), 409 ) 410 411 checkCanCancel := func(caller identity.Identity, info TaskAuthInfo) bool { 412 info.TaskID = "65aba3a3e6b99310" 413 chk := Checker{cfg: cfg, db: db, caller: caller} 414 res := chk.CheckTaskPerm(ctx, info, PermTasksCancel) 415 So(res.InternalError, ShouldBeFalse) 416 return res.Permitted 417 } 418 419 Convey("Unknown", t, func() { 420 So(checkCanCancel(unknownID, TaskAuthInfo{ 421 Realm: "project:visible-task-realm", 422 Pool: "visible-pool", 423 Submitter: authorizedID, 424 }), ShouldBeFalse) 425 }) 426 427 Convey("Submitter", t, func() { 428 So(checkCanCancel(unknownID, TaskAuthInfo{ 429 Realm: "project:doesnt-matter", 430 Pool: "doesnt-matter", 431 Submitter: unknownID, 432 }), ShouldBeTrue) 433 }) 434 435 Convey("Admin", t, func() { 436 So(checkCanCancel(adminID, TaskAuthInfo{ 437 Realm: "project:doesnt-matter", 438 Pool: "doesnt-matter", 439 Submitter: unknownID, 440 }), ShouldBeTrue) 441 }) 442 443 Convey("Via task realm", t, func() { 444 So(checkCanCancel(authorizedID, TaskAuthInfo{ 445 Realm: "project:visible-task-realm", 446 Pool: "doesnt-matter", 447 Submitter: unknownID, 448 }), ShouldBeTrue) 449 450 So(checkCanCancel(authorizedID, TaskAuthInfo{ 451 Realm: "project:hidden-task-realm", 452 Pool: "doesnt-matter", 453 Submitter: unknownID, 454 }), ShouldBeFalse) 455 }) 456 457 Convey("Via pool realm", t, func() { 458 So(checkCanCancel(authorizedID, TaskAuthInfo{ 459 Realm: "project:doesnt-matter", 460 Pool: "visible-pool", 461 Submitter: unknownID, 462 }), ShouldBeTrue) 463 464 So(checkCanCancel(authorizedID, TaskAuthInfo{ 465 Realm: "project:doesnt-matter", 466 Pool: "hidden-pool", 467 Submitter: unknownID, 468 }), ShouldBeFalse) 469 }) 470 471 Convey("Via bot realm", t, func() { 472 So(checkCanCancel(authorizedID, TaskAuthInfo{ 473 Realm: "project:doesnt-matter", 474 BotID: "visible-bot", 475 Submitter: unknownID, 476 }), ShouldBeTrue) 477 478 So(checkCanCancel(authorizedID, TaskAuthInfo{ 479 Realm: "project:doesnt-matter", 480 BotID: "hidden-bot", 481 Submitter: unknownID, 482 }), ShouldBeFalse) 483 }) 484 485 Convey("Error message", t, func() { 486 chk := Checker{cfg: cfg, db: db, caller: authorizedID} 487 res := chk.CheckTaskPerm(ctx, TaskAuthInfo{ 488 TaskID: "65aba3a3e6b99310", 489 Realm: "project:doesnt-matter", 490 BotID: "hidden-bot", 491 Submitter: unknownID, 492 }, PermTasksCancel) 493 So(res.InternalError, ShouldBeFalse) 494 err := res.ToGrpcErr() 495 So(err, ShouldHaveGRPCStatus, codes.PermissionDenied) 496 So(err, ShouldErrLike, `the caller "user:authorized@example.com" doesn't have `+ 497 `permission "swarming.tasks.cancel" for the task "65aba3a3e6b99310"`) 498 }) 499 } 500 501 //////////////////////////////////////////////////////////////////////////////// 502 503 // allPermissions returns all registered Swarming permissions. 504 func allPermissions() []realms.Permission { 505 var perms []realms.Permission 506 for perm := range realms.RegisteredPermissions() { 507 if strings.HasPrefix(perm.String(), "swarming.") { 508 perms = append(perms, perm) 509 } 510 } 511 sort.Slice(perms, func(i, j int) bool { return perms[i].String() < perms[j].String() }) 512 return perms 513 } 514 515 // assertSame fails if permissions sets have differences. 516 func assertSame(got, want []realms.Permission) { 517 asSet := func(x []realms.Permission) []string { 518 s := stringset.New(len(x)) 519 for _, p := range x { 520 s.Add(p.String()) 521 } 522 return s.ToSortedSlice() 523 } 524 So(asSet(got), ShouldResemble, asSet(want)) 525 } 526 527 // mockedConfig prepares a queryable config. 528 func mockedConfig(settings *configpb.AuthSettings, pools map[string]string, bots map[string][]string) *cfg.Config { 529 // Note this logic is the same as in rpcs.MockConfigs, but we can't use it 530 // directly due to import cycles. It is a relatively small chunk of code, it's 531 // not worth extracting into a separate package. So just repeat it. 532 // 533 // It essentially uses the real config loading implementation, just on top of 534 // fake temporary datastore. 535 536 // Prepare minimal pools.cfg. 537 var poolpb []*configpb.Pool 538 for pool, realm := range pools { 539 poolpb = append(poolpb, &configpb.Pool{ 540 Name: []string{pool}, 541 Realm: realm, 542 }) 543 } 544 sort.Slice(poolpb, func(i, j int) bool { return poolpb[i].Name[0] < poolpb[j].Name[0] }) 545 546 // Prepare minimal bots.cfg. 547 var botpb []*configpb.BotGroup 548 for botID, pools := range bots { 549 var dims []string 550 for _, pool := range pools { 551 dims = append(dims, "pool:"+pool) 552 } 553 botpb = append(botpb, &configpb.BotGroup{ 554 BotId: []string{botID}, 555 Dimensions: dims, 556 Auth: []*configpb.BotAuth{ // required field 557 { 558 RequireLuciMachineToken: true, 559 }, 560 }, 561 }) 562 } 563 sort.Slice(botpb, func(i, j int) bool { return botpb[i].BotId[0] < botpb[j].BotId[0] }) 564 565 // Convert configs to raw proto text files. 566 files := make(cfgmem.Files) 567 putPb := func(path string, msg proto.Message) { 568 if msg != nil { 569 blob, err := prototext.Marshal(msg) 570 if err != nil { 571 panic(err) 572 } 573 files[path] = string(blob) 574 } 575 } 576 putPb("settings.cfg", &configpb.SettingsCfg{Auth: settings}) 577 putPb("pools.cfg", &configpb.PoolsCfg{Pool: poolpb}) 578 putPb("bots.cfg", &configpb.BotsCfg{ 579 TrustedDimensions: []string{"pool"}, 580 BotGroup: botpb, 581 }) 582 583 // Put new configs into a temporary fake datastore. 584 ctx := memory.Use(context.Background()) 585 err := cfg.UpdateConfigs(cfgclient.Use(ctx, cfgmem.New(map[config.Set]cfgmem.Files{ 586 "services/${appid}": files, 587 }))) 588 if err != nil { 589 panic(err) 590 } 591 592 // Load them back in a queriable form. 593 p, err := cfg.NewProvider(ctx) 594 if err != nil { 595 panic(err) 596 } 597 return p.Config(ctx) 598 }