github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/dispatch/graph/reachableresources_test.go (about) 1 package graph 2 3 import ( 4 "cmp" 5 "context" 6 "fmt" 7 "slices" 8 "sort" 9 "strconv" 10 "sync" 11 "testing" 12 13 "github.com/stretchr/testify/require" 14 "go.uber.org/goleak" 15 "golang.org/x/sync/errgroup" 16 17 "github.com/authzed/spicedb/internal/datastore/memdb" 18 "github.com/authzed/spicedb/internal/dispatch" 19 "github.com/authzed/spicedb/internal/dispatch/caching" 20 "github.com/authzed/spicedb/internal/dispatch/keys" 21 log "github.com/authzed/spicedb/internal/logging" 22 datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" 23 "github.com/authzed/spicedb/internal/testfixtures" 24 "github.com/authzed/spicedb/internal/testutil" 25 "github.com/authzed/spicedb/pkg/datastore" 26 "github.com/authzed/spicedb/pkg/datastore/options" 27 "github.com/authzed/spicedb/pkg/genutil/mapz" 28 core "github.com/authzed/spicedb/pkg/proto/core/v1" 29 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 30 "github.com/authzed/spicedb/pkg/tuple" 31 ) 32 33 type reachableResource struct { 34 onr string 35 hasPermission bool 36 } 37 38 func reachable(onr *core.ObjectAndRelation, hasPermission bool) reachableResource { 39 return reachableResource{ 40 tuple.StringONR(onr), hasPermission, 41 } 42 } 43 44 func TestSimpleReachableResources(t *testing.T) { 45 defer goleak.VerifyNone(t, goleakIgnores...) 46 47 testCases := []struct { 48 start *core.RelationReference 49 target *core.ObjectAndRelation 50 reachable []reachableResource 51 }{ 52 { 53 RR("document", "view"), 54 ONR("user", "unknown", "..."), 55 []reachableResource{}, 56 }, 57 { 58 RR("document", "view"), 59 ONR("user", "eng_lead", "..."), 60 []reachableResource{ 61 reachable(ONR("document", "masterplan", "view"), true), 62 }, 63 }, 64 { 65 RR("document", "view"), 66 ONR("user", "multiroleguy", "..."), 67 []reachableResource{ 68 reachable(ONR("document", "specialplan", "view"), true), 69 }, 70 }, 71 { 72 RR("document", "view"), 73 ONR("user", "legal", "..."), 74 []reachableResource{ 75 reachable(ONR("document", "companyplan", "view"), true), 76 reachable(ONR("document", "masterplan", "view"), true), 77 }, 78 }, 79 { 80 RR("document", "view"), 81 ONR("user", "multiroleguy", "..."), 82 []reachableResource{ 83 reachable(ONR("document", "specialplan", "view"), true), 84 }, 85 }, 86 { 87 RR("document", "view_and_edit"), 88 ONR("user", "multiroleguy", "..."), 89 []reachableResource{ 90 reachable(ONR("document", "specialplan", "view_and_edit"), false), 91 }, 92 }, 93 { 94 RR("document", "view_and_edit"), 95 ONR("user", "missingrolegal", "..."), 96 []reachableResource{ 97 reachable(ONR("document", "specialplan", "view_and_edit"), false), 98 }, 99 }, 100 { 101 RR("document", "view"), 102 ONR("user", "villan", "..."), 103 []reachableResource{}, 104 }, 105 { 106 RR("document", "view"), 107 ONR("user", "owner", "..."), 108 []reachableResource{ 109 reachable(ONR("document", "companyplan", "view"), true), 110 reachable(ONR("document", "masterplan", "view"), true), 111 reachable(ONR("document", "ownerplan", "view"), true), 112 }, 113 }, 114 { 115 RR("folder", "view"), 116 ONR("folder", "company", "view"), 117 []reachableResource{ 118 reachable(ONR("folder", "strategy", "view"), true), 119 reachable(ONR("folder", "company", "view"), true), 120 }, 121 }, 122 { 123 RR("document", "view"), 124 ONR("user", "chief_financial_officer", "..."), 125 []reachableResource{ 126 reachable(ONR("document", "healthplan", "view"), true), 127 reachable(ONR("document", "masterplan", "view"), true), 128 }, 129 }, 130 { 131 RR("folder", "view"), 132 ONR("user", "owner", "..."), 133 []reachableResource{ 134 reachable(ONR("folder", "company", "view"), true), 135 reachable(ONR("folder", "strategy", "view"), true), 136 }, 137 }, 138 { 139 RR("document", "view"), 140 ONR("document", "masterplan", "view"), 141 []reachableResource{ 142 reachable(ONR("document", "masterplan", "view"), true), 143 }, 144 }, 145 } 146 147 for _, tc := range testCases { 148 name := fmt.Sprintf( 149 "%s#%s->%s", 150 tc.start.Namespace, 151 tc.start.Relation, 152 tuple.StringONR(tc.target), 153 ) 154 155 tc := tc 156 t.Run(name, func(t *testing.T) { 157 defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) 158 159 require := require.New(t) 160 161 ctx, dispatcher, revision := newLocalDispatcher(t) 162 defer dispatcher.Close() 163 164 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) 165 err := dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 166 ResourceRelation: tc.start, 167 SubjectRelation: &core.RelationReference{ 168 Namespace: tc.target.Namespace, 169 Relation: tc.target.Relation, 170 }, 171 SubjectIds: []string{tc.target.ObjectId}, 172 Metadata: &v1.ResolverMeta{ 173 AtRevision: revision.String(), 174 DepthRemaining: 50, 175 }, 176 OptionalLimit: 100000000, 177 }, stream) 178 179 results := []reachableResource{} 180 for _, streamResult := range stream.Results() { 181 results = append(results, reachableResource{ 182 tuple.StringONR(&core.ObjectAndRelation{ 183 Namespace: tc.start.Namespace, 184 ObjectId: streamResult.Resource.ResourceId, 185 Relation: tc.start.Relation, 186 }), 187 streamResult.Resource.ResultStatus == v1.ReachableResource_HAS_PERMISSION, 188 }) 189 } 190 dispatcher.Close() 191 192 slices.SortFunc(results, byONRAndPermission) 193 slices.SortFunc(tc.reachable, byONRAndPermission) 194 195 require.NoError(err) 196 require.Equal(tc.reachable, results, "Found: %v, Expected: %v", results, tc.reachable) 197 }) 198 } 199 } 200 201 func TestMaxDepthreachableResources(t *testing.T) { 202 require := require.New(t) 203 204 ctx, dispatcher, revision := newLocalDispatcher(t) 205 206 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) 207 err := dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 208 ResourceRelation: RR("document", "view"), 209 SubjectRelation: RR("user", "..."), 210 SubjectIds: []string{"legal"}, 211 Metadata: &v1.ResolverMeta{ 212 AtRevision: revision.String(), 213 DepthRemaining: 0, 214 }, 215 OptionalLimit: 100000000, 216 }, stream) 217 218 require.Error(err) 219 } 220 221 func byONRAndPermission(a, b reachableResource) int { 222 return cmp.Compare( 223 fmt.Sprintf("%s:%v", a.onr, a.hasPermission), 224 fmt.Sprintf("%s:%v", b.onr, b.hasPermission), 225 ) 226 } 227 228 func BenchmarkReachableResources(b *testing.B) { 229 testCases := []struct { 230 start *core.RelationReference 231 target *core.ObjectAndRelation 232 }{ 233 { 234 RR("document", "view"), 235 ONR("user", "legal", "..."), 236 }, 237 { 238 RR("document", "view"), 239 ONR("user", "multiroleguy", "..."), 240 }, 241 { 242 RR("document", "view_and_edit"), 243 ONR("user", "multiroleguy", "..."), 244 }, 245 { 246 RR("document", "view"), 247 ONR("user", "owner", "..."), 248 }, 249 { 250 RR("folder", "view"), 251 ONR("user", "owner", "..."), 252 }, 253 } 254 255 for _, tc := range testCases { 256 name := fmt.Sprintf( 257 "%s#%s->%s", 258 tc.start.Namespace, 259 tc.start.Relation, 260 tuple.StringONR(tc.target), 261 ) 262 263 require := require.New(b) 264 rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 265 require.NoError(err) 266 267 ds, revision := testfixtures.StandardDatastoreWithData(rawDS, require) 268 269 dispatcher := NewLocalOnlyDispatcher(10) 270 271 ctx := datastoremw.ContextWithHandle(context.Background()) 272 require.NoError(datastoremw.SetInContext(ctx, ds)) 273 274 tc := tc 275 b.Run(name, func(t *testing.B) { 276 for n := 0; n < b.N; n++ { 277 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) 278 err := dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 279 ResourceRelation: tc.start, 280 SubjectRelation: &core.RelationReference{ 281 Namespace: tc.target.Namespace, 282 Relation: tc.target.Relation, 283 }, 284 SubjectIds: []string{tc.target.ObjectId}, 285 Metadata: &v1.ResolverMeta{ 286 AtRevision: revision.String(), 287 DepthRemaining: 50, 288 }, 289 }, stream) 290 require.NoError(err) 291 292 results := []*core.ObjectAndRelation{} 293 for _, streamResult := range stream.Results() { 294 results = append(results, &core.ObjectAndRelation{ 295 Namespace: tc.start.Namespace, 296 ObjectId: streamResult.Resource.ResourceId, 297 Relation: tc.start.Relation, 298 }) 299 } 300 require.GreaterOrEqual(len(results), 0) 301 } 302 }) 303 } 304 } 305 306 func TestCaveatedReachableResources(t *testing.T) { 307 testCases := []struct { 308 name string 309 schema string 310 relationships []*core.RelationTuple 311 start *core.RelationReference 312 target *core.ObjectAndRelation 313 reachable []reachableResource 314 }{ 315 { 316 "unknown subject", 317 `definition user {} 318 319 definition document { 320 relation viewer: user 321 permission view = viewer 322 } 323 `, 324 nil, 325 RR("document", "view"), 326 ONR("user", "unknown", "..."), 327 []reachableResource{}, 328 }, 329 { 330 "no caveats", 331 `definition user {} 332 333 definition document { 334 relation viewer: user 335 permission view = viewer 336 } 337 `, 338 []*core.RelationTuple{ 339 tuple.MustParse("document:foo#viewer@user:tom"), 340 }, 341 RR("document", "view"), 342 ONR("user", "tom", "..."), 343 []reachableResource{{"document:foo#view", true}}, 344 }, 345 { 346 "basic caveats", 347 `definition user {} 348 349 definition document { 350 relation viewer: user with testcaveat 351 permission view = viewer 352 } 353 354 caveat testcaveat(somecondition int) { 355 somecondition == 42 356 } 357 `, 358 []*core.RelationTuple{ 359 tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), 360 }, 361 RR("document", "view"), 362 ONR("user", "tom", "..."), 363 []reachableResource{{"document:foo#view", false}}, 364 }, 365 { 366 "nested caveats", 367 `definition user {} 368 369 definition organization { 370 relation viewer: user with testcaveat 371 } 372 373 definition document { 374 relation parent: organization 375 permission view = parent->viewer 376 } 377 378 caveat testcaveat(somecondition int) { 379 somecondition == 42 380 } 381 `, 382 []*core.RelationTuple{ 383 tuple.MustParse("document:somedoc#parent@organization:foo"), 384 tuple.MustWithCaveat(tuple.MustParse("organization:foo#viewer@user:tom"), "testcaveat"), 385 }, 386 RR("document", "view"), 387 ONR("user", "tom", "..."), 388 []reachableResource{{"document:somedoc#view", false}}, 389 }, 390 { 391 "arrowed caveats", 392 `definition user {} 393 394 definition organization { 395 relation viewer: user 396 } 397 398 definition document { 399 relation parent: organization with testcaveat 400 permission view = parent->viewer 401 } 402 403 caveat testcaveat(somecondition int) { 404 somecondition == 42 405 } 406 `, 407 []*core.RelationTuple{ 408 tuple.MustParse("organization:foo#viewer@user:tom"), 409 tuple.MustWithCaveat(tuple.MustParse("document:somedoc#parent@organization:foo"), "testcaveat"), 410 }, 411 RR("document", "view"), 412 ONR("user", "tom", "..."), 413 []reachableResource{{"document:somedoc#view", false}}, 414 }, 415 { 416 "mixture", 417 `definition user {} 418 419 definition document { 420 relation viewer: user | user with testcaveat 421 permission view = viewer 422 } 423 424 caveat testcaveat(somecondition int) { 425 somecondition == 42 426 } 427 `, 428 []*core.RelationTuple{ 429 tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), 430 tuple.MustParse("document:bar#viewer@user:tom"), 431 }, 432 RR("document", "view"), 433 ONR("user", "tom", "..."), 434 []reachableResource{ 435 {"document:foo#view", false}, 436 {"document:bar#view", true}, 437 }, 438 }, 439 { 440 "intersection example", 441 `definition user {} 442 443 definition document { 444 relation viewer: user | user with testcaveat 445 relation editor: user 446 permission can_do_things = viewer & editor 447 } 448 449 caveat testcaveat(somecondition int) { 450 somecondition == 42 451 } 452 `, 453 []*core.RelationTuple{ 454 tuple.MustParse("document:bar#editor@user:tom"), 455 tuple.MustParse("document:bar#viewer@user:tom"), 456 tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), 457 tuple.MustParse("document:foo#editor@user:tom"), 458 }, 459 RR("document", "can_do_things"), 460 ONR("user", "tom", "..."), 461 []reachableResource{ 462 {"document:foo#can_do_things", false}, 463 {"document:bar#can_do_things", false}, 464 }, 465 }, 466 { 467 "intersection reverse example", 468 `definition user {} 469 470 definition document { 471 relation viewer: user | user with testcaveat 472 relation editor: user 473 permission can_do_things = editor & viewer 474 } 475 476 caveat testcaveat(somecondition int) { 477 somecondition == 42 478 } 479 `, 480 []*core.RelationTuple{ 481 tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), 482 tuple.MustParse("document:foo#editor@user:tom"), 483 }, 484 RR("document", "can_do_things"), 485 ONR("user", "tom", "..."), 486 []reachableResource{ 487 {"document:foo#can_do_things", false}, 488 }, 489 }, 490 { 491 "exclusion example", 492 `definition user {} 493 494 definition document { 495 relation viewer: user | user with testcaveat 496 relation banned: user 497 permission can_do_things = viewer - banned 498 } 499 500 caveat testcaveat(somecondition int) { 501 somecondition == 42 502 } 503 `, 504 []*core.RelationTuple{ 505 tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), 506 tuple.MustParse("document:foo#banned@user:tom"), 507 }, 508 RR("document", "can_do_things"), 509 ONR("user", "tom", "..."), 510 []reachableResource{ 511 {"document:foo#can_do_things", false}, 512 }, 513 }, 514 { 515 "short circuited arrow example", 516 `definition user {} 517 518 definition folder { 519 relation viewer: user 520 } 521 522 definition document { 523 relation folder: folder | folder with testcaveat 524 permission view = folder->viewer 525 } 526 527 caveat testcaveat(somecondition int) { 528 somecondition == 42 529 } 530 `, 531 []*core.RelationTuple{ 532 tuple.MustWithCaveat(tuple.MustParse("document:foo#folder@folder:maybe"), "testcaveat"), 533 tuple.MustParse("document:foo#folder@folder:always"), 534 tuple.MustParse("folder:always#viewer@user:tom"), 535 tuple.MustParse("folder:maybe#viewer@user:tom"), 536 }, 537 RR("document", "view"), 538 ONR("user", "tom", "..."), 539 []reachableResource{ 540 {"document:foo#view", true}, 541 }, 542 }, 543 { 544 "multiple relation example", 545 `definition user {} 546 547 definition document { 548 relation viewer: user with testcaveat 549 relation editor: user 550 permission view = viewer + editor 551 } 552 553 caveat testcaveat(somecondition int) { 554 somecondition == 42 555 } 556 `, 557 []*core.RelationTuple{ 558 tuple.MustWithCaveat(tuple.MustParse("document:foo#viewer@user:tom"), "testcaveat"), 559 tuple.MustParse("document:foo#editor@user:tom"), 560 }, 561 RR("document", "view"), 562 ONR("user", "tom", "..."), 563 []reachableResource{ 564 {"document:foo#view", true}, 565 {"document:foo#view", false}, 566 }, 567 }, 568 } 569 570 for _, tc := range testCases { 571 tc := tc 572 t.Run(tc.name, func(t *testing.T) { 573 require := require.New(t) 574 575 dispatcher := NewLocalOnlyDispatcher(10) 576 577 ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 578 require.NoError(err) 579 580 ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships(ds, tc.schema, tc.relationships, require) 581 582 ctx := datastoremw.ContextWithHandle(context.Background()) 583 require.NoError(datastoremw.SetInContext(ctx, ds)) 584 585 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) 586 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 587 ResourceRelation: tc.start, 588 SubjectRelation: &core.RelationReference{ 589 Namespace: tc.target.Namespace, 590 Relation: tc.target.Relation, 591 }, 592 SubjectIds: []string{tc.target.ObjectId}, 593 Metadata: &v1.ResolverMeta{ 594 AtRevision: revision.String(), 595 DepthRemaining: 50, 596 }, 597 }, stream) 598 599 results := []reachableResource{} 600 for _, streamResult := range stream.Results() { 601 results = append(results, reachableResource{ 602 tuple.StringONR(&core.ObjectAndRelation{ 603 Namespace: tc.start.Namespace, 604 ObjectId: streamResult.Resource.ResourceId, 605 Relation: tc.start.Relation, 606 }), 607 streamResult.Resource.ResultStatus == v1.ReachableResource_HAS_PERMISSION, 608 }) 609 } 610 slices.SortFunc(results, byONRAndPermission) 611 slices.SortFunc(tc.reachable, byONRAndPermission) 612 613 require.NoError(err) 614 require.Equal(tc.reachable, results, "Found: %v, Expected: %v", results, tc.reachable) 615 }) 616 } 617 } 618 619 func TestReachableResourcesWithConsistencyLimitOf1(t *testing.T) { 620 defer goleak.VerifyNone(t, goleakIgnores...) 621 622 ctx, dispatcher, revision := newLocalDispatcherWithConcurrencyLimit(t, 1) 623 defer dispatcher.Close() 624 625 target := ONR("user", "owner", "...") 626 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) 627 err := dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 628 ResourceRelation: RR("folder", "view"), 629 SubjectRelation: &core.RelationReference{ 630 Namespace: target.Namespace, 631 Relation: target.Relation, 632 }, 633 SubjectIds: []string{target.ObjectId}, 634 Metadata: &v1.ResolverMeta{ 635 AtRevision: revision.String(), 636 DepthRemaining: 50, 637 }, 638 OptionalLimit: 100000000, 639 }, stream) 640 require.NoError(t, err) 641 642 for range stream.Results() { 643 // Break early 644 break 645 } 646 dispatcher.Close() 647 } 648 649 func TestReachableResourcesMultipleEntrypointEarlyCancel(t *testing.T) { 650 defer goleak.VerifyNone(t, goleakIgnores...) 651 652 rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 653 require.NoError(t, err) 654 655 testRels := make([]*core.RelationTuple, 0) 656 for i := 0; i < 25; i++ { 657 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%d#viewer@user:tom", i))) 658 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%d#namespace@namespace:ns%d", i, i))) 659 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("namespace:ns%d#parent@namespace:ns%d", i+1, i))) 660 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("organization:org%d#member@user:tom", i))) 661 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("namespace:ns%d#viewer@user:tom", i))) 662 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:someresource#org@organization:org%d", i))) 663 } 664 665 ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships( 666 rawDS, 667 ` 668 definition user {} 669 670 definition organization { 671 relation direct_member: user 672 relation admin: user 673 permission member = direct_member + admin 674 } 675 676 definition namespace { 677 relation parent: namespace 678 relation viewer: user 679 relation admin: user 680 permission view = viewer + admin + parent->view 681 } 682 683 definition resource { 684 relation org: organization 685 relation admin: user 686 relation writer: user 687 relation viewer: user 688 relation namespace: namespace 689 permission view = viewer + writer + admin + namespace->view + org->member 690 } 691 `, 692 testRels, 693 require.New(t), 694 ) 695 dispatcher := NewLocalOnlyDispatcher(2) 696 697 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 698 require.NoError(t, datastoremw.SetInContext(ctx, ds)) 699 700 // Dispatch reachable resources but terminate the stream early by canceling. 701 ctxWithCancel, cancel := context.WithCancel(ctx) 702 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) 703 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 704 ResourceRelation: RR("resource", "view"), 705 SubjectRelation: &core.RelationReference{ 706 Namespace: "user", 707 Relation: "...", 708 }, 709 SubjectIds: []string{"tom"}, 710 Metadata: &v1.ResolverMeta{ 711 AtRevision: revision.String(), 712 DepthRemaining: 50, 713 }, 714 }, stream) 715 require.NoError(t, err) 716 717 for range stream.Results() { 718 // Break early 719 break 720 } 721 722 // Cancel, which should terminate all the existing goroutines in the dispatch. 723 cancel() 724 } 725 726 func TestReachableResourcesCursors(t *testing.T) { 727 defer goleak.VerifyNone(t, goleakIgnores...) 728 729 rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 730 require.NoError(t, err) 731 732 testRels := make([]*core.RelationTuple, 0) 733 734 // tom and sarah have access via a single role on each. 735 for i := 0; i < 410; i++ { 736 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%d#viewer@user:tom", i))) 737 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%d#editor@user:sarah", i))) 738 } 739 740 // fred is half on viewer and half on editor. 741 for i := 0; i < 410; i++ { 742 if i > 200 { 743 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%d#viewer@user:fred", i))) 744 } else { 745 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%d#editor@user:fred", i))) 746 } 747 } 748 749 ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships( 750 rawDS, 751 ` 752 definition user {} 753 754 definition resource { 755 relation editor: user 756 relation viewer: user 757 permission edit = editor 758 permission view = viewer + edit 759 } 760 `, 761 testRels, 762 require.New(t), 763 ) 764 765 subjects := []string{"tom", "sarah", "fred"} 766 767 for _, subject := range subjects { 768 t.Run(subject, func(t *testing.T) { 769 dispatcher := NewLocalOnlyDispatcher(2) 770 771 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 772 require.NoError(t, datastoremw.SetInContext(ctx, ds)) 773 774 // Dispatch reachable resources but stop reading after the second chunk of results. 775 ctxWithCancel, cancel := context.WithCancel(ctx) 776 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) 777 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 778 ResourceRelation: RR("resource", "view"), 779 SubjectRelation: &core.RelationReference{ 780 Namespace: "user", 781 Relation: "...", 782 }, 783 SubjectIds: []string{"tom"}, 784 Metadata: &v1.ResolverMeta{ 785 AtRevision: revision.String(), 786 DepthRemaining: 50, 787 }, 788 }, stream) 789 require.NoError(t, err) 790 defer cancel() 791 792 foundResources := mapz.NewSet[string]() 793 var cursor *v1.Cursor 794 795 for index, result := range stream.Results() { 796 require.True(t, foundResources.Add(result.Resource.ResourceId)) 797 798 // Break on the 200th result. 799 if index == 199 { 800 cursor = result.AfterResponseCursor 801 cancel() 802 break 803 } 804 } 805 806 // Ensure we've found a cursor and that we got 200 resources back in the first + second result. 807 require.NotNil(t, cursor, "got no cursor and %d results", foundResources.Len()) 808 require.Equal(t, foundResources.Len(), 200) 809 810 // Call reachable resources again with the cursor, which should continue in the second result 811 // and then move forward from there. 812 stream2 := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) 813 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 814 ResourceRelation: RR("resource", "view"), 815 SubjectRelation: &core.RelationReference{ 816 Namespace: "user", 817 Relation: "...", 818 }, 819 SubjectIds: []string{"tom"}, 820 Metadata: &v1.ResolverMeta{ 821 AtRevision: revision.String(), 822 DepthRemaining: 50, 823 }, 824 OptionalCursor: cursor, 825 }, stream2) 826 require.NoError(t, err) 827 828 count := 0 829 for _, result := range stream2.Results() { 830 count++ 831 foundResources.Insert(result.Resource.ResourceId) 832 } 833 require.LessOrEqual(t, count, 310) 834 835 // Ensure *all* results were found. 836 for i := 0; i < 410; i++ { 837 require.True(t, foundResources.Has("res"+strconv.Itoa(i)), "missing res%d", i) 838 } 839 }) 840 } 841 } 842 843 func TestReachableResourcesPaginationWithLimit(t *testing.T) { 844 defer goleak.VerifyNone(t, goleakIgnores...) 845 846 rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 847 require.NoError(t, err) 848 849 testRels := make([]*core.RelationTuple, 0) 850 851 for i := 0; i < 410; i++ { 852 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) 853 } 854 855 ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships( 856 rawDS, 857 ` 858 definition user {} 859 860 definition resource { 861 relation editor: user 862 relation viewer: user 863 permission edit = editor 864 permission view = viewer + edit 865 } 866 `, 867 testRels, 868 require.New(t), 869 ) 870 871 for _, limit := range []uint32{1, 10, 50, 100, 150, 250, 500} { 872 limit := limit 873 t.Run(fmt.Sprintf("limit-%d", limit), func(t *testing.T) { 874 dispatcher := NewLocalOnlyDispatcher(2) 875 var cursor *v1.Cursor 876 foundResources := mapz.NewSet[string]() 877 878 for i := 0; i < (410/int(limit))+1; i++ { 879 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 880 require.NoError(t, datastoremw.SetInContext(ctx, ds)) 881 882 ctxWithCancel, cancel := context.WithCancel(ctx) 883 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) 884 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 885 ResourceRelation: RR("resource", "view"), 886 SubjectRelation: &core.RelationReference{ 887 Namespace: "user", 888 Relation: "...", 889 }, 890 SubjectIds: []string{"tom"}, 891 Metadata: &v1.ResolverMeta{ 892 AtRevision: revision.String(), 893 DepthRemaining: 50, 894 }, 895 OptionalCursor: cursor, 896 OptionalLimit: limit, 897 }, stream) 898 require.NoError(t, err) 899 defer cancel() 900 901 newFound := 0 902 existingCursor := cursor 903 for _, result := range stream.Results() { 904 require.True(t, foundResources.Add(result.Resource.ResourceId), "found duplicate %s for iteration %d with cursor %s", result.Resource.ResourceId, i, existingCursor) 905 newFound++ 906 907 cursor = result.AfterResponseCursor 908 } 909 require.LessOrEqual(t, newFound, int(limit)) 910 if newFound == 0 { 911 break 912 } 913 } 914 915 // Ensure *all* results were found. 916 for i := 0; i < 410; i++ { 917 resourceID := fmt.Sprintf("res%03d", i) 918 require.True(t, foundResources.Has(resourceID), "missing %s", resourceID) 919 } 920 }) 921 } 922 } 923 924 func TestReachableResourcesWithQueryError(t *testing.T) { 925 defer goleak.VerifyNone(t, goleakIgnores...) 926 927 rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 928 require.NoError(t, err) 929 930 testRels := make([]*core.RelationTuple, 0) 931 932 for i := 0; i < 410; i++ { 933 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) 934 } 935 936 baseds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships( 937 rawDS, 938 ` 939 definition user {} 940 941 definition resource { 942 relation editor: user 943 relation viewer: user 944 permission edit = editor 945 permission view = viewer + edit 946 } 947 `, 948 testRels, 949 require.New(t), 950 ) 951 952 dispatcher := NewLocalOnlyDispatcher(2) 953 954 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 955 956 bds := breakingDatastore{baseds} 957 require.NoError(t, datastoremw.SetInContext(ctx, bds)) 958 959 ctxWithCancel, cancel := context.WithCancel(ctx) 960 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) 961 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 962 ResourceRelation: RR("resource", "view"), 963 SubjectRelation: &core.RelationReference{ 964 Namespace: "user", 965 Relation: "...", 966 }, 967 SubjectIds: []string{"tom"}, 968 Metadata: &v1.ResolverMeta{ 969 AtRevision: revision.String(), 970 DepthRemaining: 50, 971 }, 972 }, stream) 973 require.Error(t, err) 974 defer cancel() 975 } 976 977 type breakingDatastore struct { 978 datastore.Datastore 979 } 980 981 func (bds breakingDatastore) SnapshotReader(rev datastore.Revision) datastore.Reader { 982 delegate := bds.Datastore.SnapshotReader(rev) 983 return &breakingReader{Reader: delegate, counter: 0, lock: sync.Mutex{}} 984 } 985 986 type breakingReader struct { 987 datastore.Reader 988 counter int 989 lock sync.Mutex 990 } 991 992 func (br *breakingReader) ReverseQueryRelationships( 993 ctx context.Context, 994 subjectsFilter datastore.SubjectsFilter, 995 options ...options.ReverseQueryOptionsOption, 996 ) (datastore.RelationshipIterator, error) { 997 br.lock.Lock() 998 br.counter++ 999 current := br.counter 1000 br.lock.Unlock() 1001 if current > 1 { 1002 return nil, fmt.Errorf("some sort of error") 1003 } 1004 return br.Reader.ReverseQueryRelationships(ctx, subjectsFilter, options...) 1005 } 1006 1007 func TestReachableResourcesOverSchema(t *testing.T) { 1008 testCases := []struct { 1009 name string 1010 schema string 1011 relationships []*core.RelationTuple 1012 permission *core.RelationReference 1013 subject *core.ObjectAndRelation 1014 expectedResourceIDs []string 1015 }{ 1016 { 1017 "basic union", 1018 `definition user {} 1019 1020 definition document { 1021 relation editor: user 1022 relation viewer: user 1023 permission view = viewer + editor 1024 }`, 1025 testutil.JoinTuples( 1026 testutil.GenTuples("document", "viewer", "user", "tom", 1510), 1027 testutil.GenTuples("document", "editor", "user", "tom", 1510), 1028 ), 1029 RR("document", "view"), 1030 ONR("user", "tom", "..."), 1031 testutil.GenResourceIds("document", 1510), 1032 }, 1033 { 1034 "basic exclusion", 1035 `definition user {} 1036 1037 definition document { 1038 relation banned: user 1039 relation viewer: user 1040 permission view = viewer - banned 1041 }`, 1042 testutil.GenTuples("document", "viewer", "user", "tom", 1010), 1043 RR("document", "view"), 1044 ONR("user", "tom", "..."), 1045 testutil.GenResourceIds("document", 1010), 1046 }, 1047 { 1048 "basic intersection", 1049 `definition user {} 1050 1051 definition document { 1052 relation editor: user 1053 relation viewer: user 1054 permission view = viewer & editor 1055 }`, 1056 testutil.JoinTuples( 1057 testutil.GenTuples("document", "viewer", "user", "tom", 510), 1058 testutil.GenTuples("document", "editor", "user", "tom", 510), 1059 ), 1060 RR("document", "view"), 1061 ONR("user", "tom", "..."), 1062 testutil.GenResourceIds("document", 510), 1063 }, 1064 { 1065 "union and exclused union", 1066 `definition user {} 1067 1068 definition document { 1069 relation editor: user 1070 relation viewer: user 1071 relation banned: user 1072 permission can_view = viewer - banned 1073 permission view = can_view + editor 1074 }`, 1075 testutil.JoinTuples( 1076 testutil.GenTuples("document", "viewer", "user", "tom", 1310), 1077 testutil.GenTuplesWithOffset("document", "editor", "user", "tom", 1250, 1200), 1078 ), 1079 RR("document", "view"), 1080 ONR("user", "tom", "..."), 1081 testutil.GenResourceIds("document", 2450), 1082 }, 1083 { 1084 "basic caveats", 1085 `definition user {} 1086 1087 caveat somecaveat(somecondition int) { 1088 somecondition == 42 1089 } 1090 1091 definition document { 1092 relation viewer: user with somecaveat 1093 permission view = viewer 1094 }`, 1095 testutil.GenTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{"somecondition": 42}, 0, 2450), 1096 RR("document", "view"), 1097 ONR("user", "tom", "..."), 1098 testutil.GenResourceIds("document", 2450), 1099 }, 1100 { 1101 "excluded items", 1102 `definition user {} 1103 1104 definition document { 1105 relation banned: user 1106 relation viewer: user 1107 permission view = viewer - banned 1108 }`, 1109 testutil.JoinTuples( 1110 testutil.GenTuples("document", "viewer", "user", "tom", 1310), 1111 testutil.GenTuplesWithOffset("document", "banned", "user", "tom", 1210, 100), 1112 ), 1113 RR("document", "view"), 1114 ONR("user", "tom", "..."), 1115 testutil.GenResourceIds("document", 1310), 1116 }, 1117 { 1118 "basic caveats with missing field", 1119 `definition user {} 1120 1121 caveat somecaveat(somecondition int) { 1122 somecondition == 42 1123 } 1124 1125 definition document { 1126 relation viewer: user with somecaveat 1127 permission view = viewer 1128 }`, 1129 testutil.GenTuplesWithCaveat("document", "viewer", "user", "tom", "somecaveat", map[string]any{}, 0, 2450), 1130 RR("document", "view"), 1131 ONR("user", "tom", "..."), 1132 testutil.GenResourceIds("document", 2450), 1133 }, 1134 { 1135 "larger arrow dispatch", 1136 `definition user {} 1137 1138 definition folder { 1139 relation viewer: user 1140 } 1141 1142 definition document { 1143 relation folder: folder 1144 permission view = folder->viewer 1145 }`, 1146 testutil.JoinTuples( 1147 testutil.GenTuples("folder", "viewer", "user", "tom", 150), 1148 testutil.GenSubjectTuples("document", "folder", "folder", "...", 150), 1149 ), 1150 RR("document", "view"), 1151 ONR("user", "tom", "..."), 1152 testutil.GenResourceIds("document", 150), 1153 }, 1154 { 1155 "big", 1156 `definition user {} 1157 1158 definition document { 1159 relation editor: user 1160 relation viewer: user 1161 permission view = viewer + editor 1162 }`, 1163 testutil.JoinTuples( 1164 testutil.GenTuples("document", "viewer", "user", "tom", 15100), 1165 testutil.GenTuples("document", "editor", "user", "tom", 15100), 1166 ), 1167 RR("document", "view"), 1168 ONR("user", "tom", "..."), 1169 testutil.GenResourceIds("document", 15100), 1170 }, 1171 { 1172 "chunked arrow with chunked redispatch", 1173 `definition user {} 1174 1175 definition folder { 1176 relation viewer: user 1177 permission view = viewer 1178 } 1179 1180 definition document { 1181 relation parent: folder 1182 permission view = parent->view 1183 }`, 1184 (func() []*core.RelationTuple { 1185 // Generate 200 folders with tom as a viewer 1186 tuples := make([]*core.RelationTuple, 0, 200*200) 1187 for folderID := 0; folderID < 200; folderID++ { 1188 tpl := &core.RelationTuple{ 1189 ResourceAndRelation: ONR("folder", fmt.Sprintf("folder-%d", folderID), "viewer"), 1190 Subject: ONR("user", "tom", "..."), 1191 } 1192 tuples = append(tuples, tpl) 1193 1194 // Generate 200 documents for each folder. 1195 for documentID := 0; documentID < 200; documentID++ { 1196 docID := fmt.Sprintf("doc-%d-%d", folderID, documentID) 1197 tpl := &core.RelationTuple{ 1198 ResourceAndRelation: ONR("document", docID, "parent"), 1199 Subject: ONR("folder", fmt.Sprintf("folder-%d", folderID), "..."), 1200 } 1201 tuples = append(tuples, tpl) 1202 } 1203 } 1204 1205 return tuples 1206 })(), 1207 RR("document", "view"), 1208 ONR("user", "tom", "..."), 1209 (func() []string { 1210 docIDs := make([]string, 0, 200*200) 1211 for folderID := 0; folderID < 200; folderID++ { 1212 for documentID := 0; documentID < 200; documentID++ { 1213 docID := fmt.Sprintf("doc-%d-%d", folderID, documentID) 1214 docIDs = append(docIDs, docID) 1215 } 1216 } 1217 return docIDs 1218 })(), 1219 }, 1220 } 1221 1222 for _, tc := range testCases { 1223 tc := tc 1224 t.Run(tc.name, func(t *testing.T) { 1225 for _, pageSize := range []int{0, 100, 1000} { 1226 pageSize := pageSize 1227 t.Run(fmt.Sprintf("ps-%d_", pageSize), func(t *testing.T) { 1228 require := require.New(t) 1229 1230 dispatcher := NewLocalOnlyDispatcher(10) 1231 1232 ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 1233 require.NoError(err) 1234 1235 ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships(ds, tc.schema, tc.relationships, require) 1236 1237 ctx := datastoremw.ContextWithHandle(context.Background()) 1238 require.NoError(datastoremw.SetInContext(ctx, ds)) 1239 1240 foundResourceIDs := mapz.NewSet[string]() 1241 1242 var currentCursor *v1.Cursor 1243 for { 1244 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) 1245 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 1246 ResourceRelation: tc.permission, 1247 SubjectRelation: &core.RelationReference{ 1248 Namespace: tc.subject.Namespace, 1249 Relation: tc.subject.Relation, 1250 }, 1251 SubjectIds: []string{tc.subject.ObjectId}, 1252 Metadata: &v1.ResolverMeta{ 1253 AtRevision: revision.String(), 1254 DepthRemaining: 50, 1255 }, 1256 OptionalCursor: currentCursor, 1257 OptionalLimit: uint32(pageSize), 1258 }, stream) 1259 require.NoError(err) 1260 1261 if pageSize > 0 { 1262 require.LessOrEqual(len(stream.Results()), pageSize) 1263 } 1264 1265 for _, result := range stream.Results() { 1266 foundResourceIDs.Insert(result.Resource.ResourceId) 1267 currentCursor = result.AfterResponseCursor 1268 } 1269 1270 if pageSize == 0 || len(stream.Results()) < pageSize { 1271 break 1272 } 1273 } 1274 1275 foundResourceIDsSlice := foundResourceIDs.AsSlice() 1276 sort.Strings(foundResourceIDsSlice) 1277 sort.Strings(tc.expectedResourceIDs) 1278 1279 require.Equal(tc.expectedResourceIDs, foundResourceIDsSlice) 1280 }) 1281 } 1282 }) 1283 } 1284 } 1285 1286 func TestReachableResourcesWithPreCancelation(t *testing.T) { 1287 defer goleak.VerifyNone(t, goleakIgnores...) 1288 1289 rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 1290 require.NoError(t, err) 1291 1292 testRels := make([]*core.RelationTuple, 0) 1293 1294 for i := 0; i < 410; i++ { 1295 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) 1296 } 1297 1298 ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships( 1299 rawDS, 1300 ` 1301 definition user {} 1302 1303 definition resource { 1304 relation editor: user 1305 relation viewer: user 1306 permission edit = editor 1307 permission view = viewer + edit 1308 } 1309 `, 1310 testRels, 1311 require.New(t), 1312 ) 1313 1314 dispatcher := NewLocalOnlyDispatcher(2) 1315 1316 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 1317 require.NoError(t, datastoremw.SetInContext(ctx, ds)) 1318 1319 ctxWithCancel, cancel := context.WithCancel(ctx) 1320 1321 // Cancel now 1322 cancel() 1323 1324 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) 1325 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 1326 ResourceRelation: RR("resource", "view"), 1327 SubjectRelation: &core.RelationReference{ 1328 Namespace: "user", 1329 Relation: "...", 1330 }, 1331 SubjectIds: []string{"tom"}, 1332 Metadata: &v1.ResolverMeta{ 1333 AtRevision: revision.String(), 1334 DepthRemaining: 50, 1335 }, 1336 }, stream) 1337 require.Error(t, err) 1338 require.ErrorIs(t, err, context.Canceled) 1339 } 1340 1341 func TestReachableResourcesWithUnexpectedContextCancelation(t *testing.T) { 1342 defer goleak.VerifyNone(t, goleakIgnores...) 1343 1344 rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 1345 require.NoError(t, err) 1346 1347 testRels := make([]*core.RelationTuple, 0) 1348 1349 for i := 0; i < 410; i++ { 1350 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) 1351 } 1352 1353 baseds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships( 1354 rawDS, 1355 ` 1356 definition user {} 1357 1358 definition resource { 1359 relation editor: user 1360 relation viewer: user 1361 permission edit = editor 1362 permission view = viewer + edit 1363 } 1364 `, 1365 testRels, 1366 require.New(t), 1367 ) 1368 1369 dispatcher := NewLocalOnlyDispatcher(2) 1370 1371 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 1372 1373 cds := cancelingDatastore{baseds} 1374 require.NoError(t, datastoremw.SetInContext(ctx, cds)) 1375 1376 ctxWithCancel, cancel := context.WithCancel(ctx) 1377 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctxWithCancel) 1378 err = dispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 1379 ResourceRelation: RR("resource", "view"), 1380 SubjectRelation: &core.RelationReference{ 1381 Namespace: "user", 1382 Relation: "...", 1383 }, 1384 SubjectIds: []string{"tom"}, 1385 Metadata: &v1.ResolverMeta{ 1386 AtRevision: revision.String(), 1387 DepthRemaining: 50, 1388 }, 1389 }, stream) 1390 require.Error(t, err) 1391 require.ErrorIs(t, err, context.Canceled) 1392 defer cancel() 1393 } 1394 1395 type cancelingDatastore struct { 1396 datastore.Datastore 1397 } 1398 1399 func (cds cancelingDatastore) SnapshotReader(rev datastore.Revision) datastore.Reader { 1400 delegate := cds.Datastore.SnapshotReader(rev) 1401 return &cancelingReader{delegate, 0, sync.Mutex{}} 1402 } 1403 1404 type cancelingReader struct { 1405 datastore.Reader 1406 counter int 1407 lock sync.Mutex 1408 } 1409 1410 func (cr *cancelingReader) ReverseQueryRelationships( 1411 ctx context.Context, 1412 subjectsFilter datastore.SubjectsFilter, 1413 options ...options.ReverseQueryOptionsOption, 1414 ) (datastore.RelationshipIterator, error) { 1415 cr.lock.Lock() 1416 cr.counter++ 1417 current := cr.counter 1418 cr.lock.Unlock() 1419 1420 if current > 1 { 1421 return nil, context.Canceled 1422 } 1423 return cr.Reader.ReverseQueryRelationships(ctx, subjectsFilter, options...) 1424 } 1425 1426 func TestReachableResourcesWithCachingInParallelTest(t *testing.T) { 1427 defer goleak.VerifyNone(t, goleakIgnores...) 1428 1429 rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 1430 require.NoError(t, err) 1431 1432 testRels := make([]*core.RelationTuple, 0) 1433 expectedResources := mapz.NewSet[string]() 1434 1435 for i := 0; i < 410; i++ { 1436 if i < 250 { 1437 expectedResources.Insert(fmt.Sprintf("res%03d", i)) 1438 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#viewer@user:tom", i))) 1439 } 1440 1441 if i > 200 { 1442 expectedResources.Insert(fmt.Sprintf("res%03d", i)) 1443 testRels = append(testRels, tuple.MustParse(fmt.Sprintf("resource:res%03d#editor@user:tom", i))) 1444 } 1445 } 1446 1447 ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships( 1448 rawDS, 1449 ` 1450 definition user {} 1451 1452 definition resource { 1453 relation editor: user 1454 relation viewer: user 1455 permission edit = editor 1456 permission view = viewer + edit 1457 } 1458 `, 1459 testRels, 1460 require.New(t), 1461 ) 1462 1463 g := errgroup.Group{} 1464 for i := 0; i < 100; i++ { 1465 g.Go(func() error { 1466 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 1467 require.NoError(t, datastoremw.SetInContext(ctx, ds)) 1468 1469 dispatcher := NewLocalOnlyDispatcher(50) 1470 cachingDispatcher, err := caching.NewCachingDispatcher(caching.DispatchTestCache(t), false, "", &keys.CanonicalKeyHandler{}) 1471 require.NoError(t, err) 1472 1473 cachingDispatcher.SetDelegate(dispatcher) 1474 1475 stream := dispatch.NewCollectingDispatchStream[*v1.DispatchReachableResourcesResponse](ctx) 1476 err = cachingDispatcher.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 1477 ResourceRelation: RR("resource", "view"), 1478 SubjectRelation: &core.RelationReference{ 1479 Namespace: "user", 1480 Relation: "...", 1481 }, 1482 SubjectIds: []string{"tom"}, 1483 Metadata: &v1.ResolverMeta{ 1484 AtRevision: revision.String(), 1485 DepthRemaining: 50, 1486 }, 1487 }, stream) 1488 require.NoError(t, err) 1489 1490 foundResources := mapz.NewSet[string]() 1491 for _, result := range stream.Results() { 1492 foundResources.Insert(result.Resource.ResourceId) 1493 } 1494 1495 expectedResourcesSlice := expectedResources.AsSlice() 1496 foundResourcesSlice := foundResources.AsSlice() 1497 1498 sort.Strings(expectedResourcesSlice) 1499 sort.Strings(foundResourcesSlice) 1500 1501 require.Equal(t, expectedResourcesSlice, foundResourcesSlice) 1502 return nil 1503 }) 1504 } 1505 1506 require.NoError(t, g.Wait()) 1507 }