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