github.com/openfga/openfga@v1.5.4-rc1/pkg/server/test/reverse_expand.go (about) 1 package test 2 3 import ( 4 "context" 5 "errors" 6 "testing" 7 "time" 8 9 "github.com/oklog/ulid/v2" 10 openfgav1 "github.com/openfga/api/proto/openfga/v1" 11 "github.com/stretchr/testify/require" 12 13 "github.com/openfga/openfga/internal/graph" 14 "github.com/openfga/openfga/pkg/server/commands/reverseexpand" 15 "github.com/openfga/openfga/pkg/storage" 16 storagetest "github.com/openfga/openfga/pkg/storage/test" 17 "github.com/openfga/openfga/pkg/tuple" 18 "github.com/openfga/openfga/pkg/typesystem" 19 ) 20 21 func TestReverseExpand(t *testing.T, ds storage.OpenFGADatastore) { 22 tests := []struct { 23 name string 24 model string 25 tuples []string 26 request *reverseexpand.ReverseExpandRequest 27 resolveNodeLimit uint32 28 expectedResult []*reverseexpand.ReverseExpandResult 29 expectedError error 30 expectedDSQueryCount uint32 31 }{ 32 { 33 name: "basic_intersection", 34 request: &reverseexpand.ReverseExpandRequest{ 35 StoreID: ulid.Make().String(), 36 ObjectType: "document", 37 Relation: "viewer", 38 User: &reverseexpand.UserRefObject{ 39 Object: &openfgav1.Object{ 40 Type: "user", 41 Id: "jon", 42 }, 43 }, 44 ContextualTuples: []*openfgav1.TupleKey{}, 45 }, 46 model: ` 47 model 48 schema 1.1 49 50 type user 51 52 type document 53 relations 54 define allowed: [user] 55 define viewer: [user] and allowed`, 56 tuples: []string{ 57 "document:1#viewer@user:jon", 58 "document:2#viewer@user:jon", 59 "document:3#allowed@user:jon", 60 }, 61 expectedResult: []*reverseexpand.ReverseExpandResult{ 62 { 63 Object: "document:1", 64 ResultStatus: reverseexpand.RequiresFurtherEvalStatus, 65 }, 66 { 67 Object: "document:2", 68 ResultStatus: reverseexpand.RequiresFurtherEvalStatus, 69 }, 70 }, 71 expectedDSQueryCount: 1, 72 }, 73 { 74 name: "indirect_intersection", 75 request: &reverseexpand.ReverseExpandRequest{ 76 StoreID: ulid.Make().String(), 77 ObjectType: "document", 78 Relation: "viewer", 79 User: &reverseexpand.UserRefObject{ 80 Object: &openfgav1.Object{ 81 Type: "user", 82 Id: "jon", 83 }, 84 }, 85 ContextualTuples: []*openfgav1.TupleKey{}, 86 }, 87 model: ` 88 model 89 schema 1.1 90 91 type user 92 93 type folder 94 relations 95 define writer: [user] 96 define editor: [user] 97 define viewer: writer and editor 98 99 type document 100 relations 101 define parent: [folder] 102 define viewer: viewer from parent`, 103 tuples: []string{ 104 "document:1#parent@folder:X", 105 "folder:X#writer@user:jon", 106 "folder:X#editor@user:jon", 107 }, 108 expectedResult: []*reverseexpand.ReverseExpandResult{ 109 { 110 Object: "document:1", 111 ResultStatus: reverseexpand.RequiresFurtherEvalStatus, 112 }, 113 }, 114 expectedDSQueryCount: 2, 115 }, 116 117 { 118 name: "resolve_direct_relationships_with_tuples_and_contextual_tuples", 119 request: &reverseexpand.ReverseExpandRequest{ 120 StoreID: ulid.Make().String(), 121 ObjectType: "document", 122 Relation: "viewer", 123 User: &reverseexpand.UserRefObject{ 124 Object: &openfgav1.Object{ 125 Type: "user", 126 Id: "jon", 127 }, 128 }, 129 ContextualTuples: []*openfgav1.TupleKey{ 130 tuple.NewTupleKey("document:doc2", "viewer", "user:bob"), 131 tuple.NewTupleKey("document:doc3", "viewer", "user:jon"), 132 }, 133 }, 134 model: ` 135 model 136 schema 1.1 137 138 type user 139 140 type document 141 relations 142 define viewer: [user]`, 143 tuples: []string{ 144 "document:doc1#viewer@user:jon", 145 }, 146 expectedResult: []*reverseexpand.ReverseExpandResult{ 147 { 148 Object: "document:doc1", 149 ResultStatus: reverseexpand.NoFurtherEvalStatus, 150 }, 151 { 152 Object: "document:doc3", 153 ResultStatus: reverseexpand.NoFurtherEvalStatus, 154 }, 155 }, 156 expectedDSQueryCount: 1, 157 }, 158 { 159 name: "direct_relations_involving_relationships_with_users_and_usersets", 160 request: &reverseexpand.ReverseExpandRequest{ 161 StoreID: ulid.Make().String(), 162 ObjectType: "document", 163 Relation: "viewer", 164 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 165 Type: "user", 166 Id: "jon", 167 }}, 168 }, 169 model: ` 170 model 171 schema 1.1 172 173 type user 174 175 type group 176 relations 177 define member: [user] 178 179 type document 180 relations 181 define viewer: [user, group#member]`, 182 tuples: []string{ 183 "document:doc1#viewer@user:jon", 184 "document:doc2#viewer@user:bob", 185 "document:doc3#viewer@group:openfga#member", 186 "group:openfga#member@user:jon", 187 }, 188 expectedResult: []*reverseexpand.ReverseExpandResult{ 189 { 190 Object: "document:doc1", 191 ResultStatus: reverseexpand.NoFurtherEvalStatus, 192 }, 193 { 194 Object: "document:doc3", 195 ResultStatus: reverseexpand.NoFurtherEvalStatus, 196 }, 197 }, 198 expectedDSQueryCount: 3, 199 }, 200 { 201 name: "success_with_direct_relationships_and_computed_usersets", 202 request: &reverseexpand.ReverseExpandRequest{ 203 StoreID: ulid.Make().String(), 204 ObjectType: "document", 205 Relation: "viewer", 206 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 207 Type: "user", 208 Id: "jon", 209 }}, 210 }, 211 model: ` 212 model 213 schema 1.1 214 215 type user 216 217 type group 218 relations 219 define member: [user] 220 221 type document 222 relations 223 define owner: [user, group#member] 224 define viewer: owner`, 225 tuples: []string{ 226 "document:doc1#owner@user:jon", 227 "document:doc2#owner@user:bob", 228 "document:doc3#owner@group:openfga#member", 229 "group:openfga#member@user:jon", 230 }, 231 expectedResult: []*reverseexpand.ReverseExpandResult{ 232 { 233 Object: "document:doc1", 234 ResultStatus: reverseexpand.NoFurtherEvalStatus, 235 }, 236 { 237 Object: "document:doc3", 238 ResultStatus: reverseexpand.NoFurtherEvalStatus, 239 }, 240 }, 241 expectedDSQueryCount: 3, 242 }, 243 { 244 name: "success_with_many_tuples", 245 request: &reverseexpand.ReverseExpandRequest{ 246 StoreID: ulid.Make().String(), 247 ObjectType: "document", 248 Relation: "viewer", 249 User: &reverseexpand.UserRefObject{ 250 Object: &openfgav1.Object{ 251 Type: "user", 252 Id: "jon", 253 }, 254 }, 255 ContextualTuples: []*openfgav1.TupleKey{ 256 tuple.NewTupleKey("folder:folder5", "parent", "folder:folder4"), 257 tuple.NewTupleKey("folder:folder6", "viewer", "user:bob"), 258 }, 259 }, 260 model: ` 261 model 262 schema 1.1 263 264 type user 265 266 type group 267 relations 268 define member: [user, group#member] 269 270 type folder 271 relations 272 define parent: [folder] 273 define viewer: [user, group#member] or viewer from parent 274 275 type document 276 relations 277 define parent: [folder] 278 define viewer: viewer from parent`, 279 tuples: []string{ 280 "folder:folder1#viewer@user:jon", 281 "folder:folder2#parent@folder:folder1", 282 "folder:folder3#parent@folder:folder2", 283 "folder:folder4#viewer@group:eng#member", 284 "document:doc1#parent@folder:folder3", 285 "document:doc2#parent@folder:folder5", 286 "document:doc3#parent@folder:folder6", 287 "group:eng#member@group:openfga#member", 288 "group:openfga#member@user:jon", 289 }, 290 expectedResult: []*reverseexpand.ReverseExpandResult{ 291 { 292 Object: "document:doc1", 293 ResultStatus: reverseexpand.NoFurtherEvalStatus, 294 }, 295 { 296 Object: "document:doc2", 297 ResultStatus: reverseexpand.NoFurtherEvalStatus, 298 }, 299 }, 300 expectedDSQueryCount: 16, 301 }, 302 { 303 name: "resolve_objects_involved_in_recursive_hierarchy", 304 request: &reverseexpand.ReverseExpandRequest{ 305 StoreID: ulid.Make().String(), 306 ObjectType: "folder", 307 Relation: "viewer", 308 User: &reverseexpand.UserRefObject{ 309 Object: &openfgav1.Object{ 310 Type: "user", 311 Id: "jon", 312 }, 313 }, 314 }, 315 model: ` 316 model 317 schema 1.1 318 319 type user 320 321 type folder 322 relations 323 define parent: [folder] 324 define viewer: [user] or viewer from parent`, 325 tuples: []string{ 326 "folder:folder1#viewer@user:jon", 327 "folder:folder2#parent@folder:folder1", 328 "folder:folder3#parent@folder:folder2", 329 }, 330 expectedResult: []*reverseexpand.ReverseExpandResult{ 331 { 332 Object: "folder:folder1", 333 ResultStatus: reverseexpand.NoFurtherEvalStatus, 334 }, 335 { 336 Object: "folder:folder2", 337 ResultStatus: reverseexpand.NoFurtherEvalStatus, 338 }, 339 { 340 Object: "folder:folder3", 341 ResultStatus: reverseexpand.NoFurtherEvalStatus, 342 }, 343 }, 344 expectedDSQueryCount: 4, 345 }, 346 { 347 name: "resolution_depth_exceeded_failure", 348 request: &reverseexpand.ReverseExpandRequest{ 349 StoreID: ulid.Make().String(), 350 ObjectType: "folder", 351 Relation: "viewer", 352 User: &reverseexpand.UserRefObject{ 353 Object: &openfgav1.Object{ 354 Type: "user", 355 Id: "jon", 356 }, 357 }, 358 }, 359 resolveNodeLimit: 2, 360 model: ` 361 model 362 schema 1.1 363 364 type user 365 366 type folder 367 relations 368 define parent: [folder] 369 define viewer: [user] or viewer from parent`, 370 tuples: []string{ 371 "folder:folder1#viewer@user:jon", 372 "folder:folder2#parent@folder:folder1", 373 "folder:folder3#parent@folder:folder2", 374 }, 375 expectedError: graph.ErrResolutionDepthExceeded, 376 expectedDSQueryCount: 0, 377 }, 378 { 379 name: "objects_connected_to_a_userset", 380 request: &reverseexpand.ReverseExpandRequest{ 381 StoreID: ulid.Make().String(), 382 ObjectType: "group", 383 Relation: "member", 384 User: &reverseexpand.UserRefObjectRelation{ 385 ObjectRelation: &openfgav1.ObjectRelation{ 386 Object: "group:iam", 387 Relation: "member", 388 }, 389 }, 390 }, 391 model: ` 392 model 393 schema 1.1 394 395 type user 396 397 type group 398 relations 399 define member: [user, group#member]`, 400 tuples: []string{ 401 "group:opensource#member@group:eng#member", 402 "group:eng#member@group:iam#member", 403 "group:iam#member@user:jon", 404 }, 405 expectedResult: []*reverseexpand.ReverseExpandResult{ 406 { 407 Object: "group:opensource", 408 ResultStatus: reverseexpand.NoFurtherEvalStatus, 409 }, 410 { 411 Object: "group:eng", 412 ResultStatus: reverseexpand.NoFurtherEvalStatus, 413 }, 414 { 415 Object: "group:iam", 416 ResultStatus: reverseexpand.NoFurtherEvalStatus, 417 }, 418 }, 419 expectedDSQueryCount: 3, 420 }, 421 { 422 name: "objects_connected_to_a_userset_self_referencing", 423 request: &reverseexpand.ReverseExpandRequest{ 424 StoreID: ulid.Make().String(), 425 ObjectType: "group", 426 Relation: "member", 427 User: &reverseexpand.UserRefObjectRelation{ 428 ObjectRelation: &openfgav1.ObjectRelation{ 429 Object: "group:iam", 430 Relation: "member", 431 }, 432 }, 433 }, 434 model: ` 435 model 436 schema 1.1 437 438 type group 439 relations 440 define member: [group#member]`, 441 tuples: []string{ 442 "group:iam#member@group:iam#member", 443 }, 444 expectedResult: []*reverseexpand.ReverseExpandResult{ 445 { 446 Object: "group:iam", 447 ResultStatus: reverseexpand.NoFurtherEvalStatus, 448 }, 449 }, 450 expectedDSQueryCount: 2, 451 }, 452 { 453 name: "objects_connected_through_a_computed_userset_1", 454 request: &reverseexpand.ReverseExpandRequest{ 455 StoreID: ulid.Make().String(), 456 ObjectType: "document", 457 Relation: "viewer", 458 User: &reverseexpand.UserRefObject{ 459 Object: &openfgav1.Object{ 460 Type: "user", 461 Id: "jon", 462 }, 463 }, 464 }, 465 model: ` 466 model 467 schema 1.1 468 469 type user 470 471 type document 472 relations 473 define owner: [user] 474 define editor: owner 475 define viewer: [document#editor]`, 476 tuples: []string{ 477 "document:1#viewer@document:1#editor", 478 "document:1#owner@user:jon", 479 }, 480 expectedResult: []*reverseexpand.ReverseExpandResult{ 481 { 482 Object: "document:1", 483 ResultStatus: reverseexpand.NoFurtherEvalStatus, 484 }, 485 }, 486 expectedDSQueryCount: 2, 487 }, 488 { 489 name: "objects_connected_through_a_computed_userset_2", 490 request: &reverseexpand.ReverseExpandRequest{ 491 StoreID: ulid.Make().String(), 492 ObjectType: "document", 493 Relation: "viewer", 494 User: &reverseexpand.UserRefObject{ 495 Object: &openfgav1.Object{ 496 Type: "user", 497 Id: "jon", 498 }, 499 }, 500 }, 501 model: ` 502 model 503 schema 1.1 504 505 type user 506 507 type group 508 relations 509 define manager: [user] 510 define member: manager 511 512 type document 513 relations 514 define viewer: [group#member]`, 515 tuples: []string{ 516 "document:1#viewer@group:eng#member", 517 "group:eng#manager@user:jon", 518 }, 519 expectedResult: []*reverseexpand.ReverseExpandResult{ 520 { 521 Object: "document:1", 522 ResultStatus: reverseexpand.NoFurtherEvalStatus, 523 }, 524 }, 525 expectedDSQueryCount: 2, 526 }, 527 { 528 name: "objects_connected_through_a_computed_userset_3", 529 request: &reverseexpand.ReverseExpandRequest{ 530 StoreID: ulid.Make().String(), 531 ObjectType: "trial", 532 Relation: "viewer", 533 User: &reverseexpand.UserRefObject{ 534 Object: &openfgav1.Object{ 535 Type: "user", 536 Id: "fede", 537 }, 538 }, 539 }, 540 model: ` 541 model 542 schema 1.1 543 544 type user 545 546 type team 547 relations 548 define admin: [user] 549 define member: admin 550 551 type trial 552 relations 553 define editor: [team#member] 554 define viewer: editor`, 555 tuples: []string{ 556 "trial:1#editor@team:devs#member", 557 "team:devs#admin@user:fede", 558 }, 559 expectedResult: []*reverseexpand.ReverseExpandResult{ 560 { 561 Object: "trial:1", 562 ResultStatus: reverseexpand.NoFurtherEvalStatus, 563 }, 564 }, 565 expectedDSQueryCount: 2, 566 }, 567 { 568 name: "objects_connected_indirectly_through_a_ttu", 569 request: &reverseexpand.ReverseExpandRequest{ 570 StoreID: ulid.Make().String(), 571 ObjectType: "document", 572 Relation: "view", 573 User: &reverseexpand.UserRefObject{ 574 Object: &openfgav1.Object{ 575 Type: "organization", 576 Id: "2", 577 }, 578 }, 579 }, 580 model: ` 581 model 582 schema 1.1 583 584 type organization 585 relations 586 define viewer: [organization] 587 define can_view: viewer 588 589 type document 590 relations 591 define parent: [organization] 592 define view: can_view from parent`, 593 tuples: []string{ 594 "document:1#parent@organization:1", 595 "organization:1#viewer@organization:2", 596 }, 597 expectedResult: []*reverseexpand.ReverseExpandResult{ 598 { 599 Object: "document:1", 600 ResultStatus: reverseexpand.NoFurtherEvalStatus, 601 }, 602 }, 603 expectedDSQueryCount: 2, 604 }, 605 { 606 name: "directly_related_typed_wildcard", 607 request: &reverseexpand.ReverseExpandRequest{ 608 StoreID: ulid.Make().String(), 609 ObjectType: "document", 610 Relation: "viewer", 611 User: &reverseexpand.UserRefTypedWildcard{Type: "user"}, 612 }, 613 model: ` 614 model 615 schema 1.1 616 617 type user 618 619 type document 620 relations 621 define viewer: [user, user:*]`, 622 tuples: []string{ 623 "document:1#viewer@user:*", 624 "document:2#viewer@user:*", 625 "document:3#viewer@user:jon", 626 }, 627 expectedResult: []*reverseexpand.ReverseExpandResult{ 628 { 629 Object: "document:1", 630 ResultStatus: reverseexpand.NoFurtherEvalStatus, 631 }, 632 { 633 Object: "document:2", 634 ResultStatus: reverseexpand.NoFurtherEvalStatus, 635 }, 636 }, 637 expectedDSQueryCount: 1, 638 }, 639 { 640 name: "indirectly_related_typed_wildcard", 641 request: &reverseexpand.ReverseExpandRequest{ 642 StoreID: ulid.Make().String(), 643 ObjectType: "document", 644 Relation: "viewer", 645 User: &reverseexpand.UserRefTypedWildcard{Type: "user"}, 646 }, 647 model: ` 648 model 649 schema 1.1 650 651 type user 652 653 type group 654 relations 655 define member: [user:*] 656 657 type document 658 relations 659 define viewer: [group#member]`, 660 tuples: []string{ 661 "document:1#viewer@group:eng#member", 662 "document:2#viewer@group:fga#member", 663 "group:eng#member@user:*", 664 }, 665 expectedResult: []*reverseexpand.ReverseExpandResult{ 666 { 667 Object: "document:1", 668 ResultStatus: reverseexpand.NoFurtherEvalStatus, 669 }, 670 }, 671 expectedDSQueryCount: 2, 672 }, 673 { 674 name: "relationship_through_multiple_indirections", 675 request: &reverseexpand.ReverseExpandRequest{ 676 StoreID: ulid.Make().String(), 677 ObjectType: "document", 678 Relation: "viewer", 679 User: &reverseexpand.UserRefObject{ 680 Object: &openfgav1.Object{ 681 Type: "user", 682 Id: "jon", 683 }, 684 }, 685 }, 686 model: ` 687 model 688 schema 1.1 689 690 type user 691 692 type team 693 relations 694 define member: [user] 695 696 type group 697 relations 698 define member: [team#member] 699 700 type document 701 relations 702 define viewer: [group#member]`, 703 tuples: []string{ 704 "team:tigers#member@user:jon", 705 "group:eng#member@team:tigers#member", 706 "document:1#viewer@group:eng#member", 707 }, 708 expectedResult: []*reverseexpand.ReverseExpandResult{ 709 { 710 Object: "document:1", 711 ResultStatus: reverseexpand.NoFurtherEvalStatus, 712 }, 713 }, 714 expectedDSQueryCount: 3, 715 }, 716 { 717 name: "typed_wildcard_relationship_through_multiple_indirections", 718 request: &reverseexpand.ReverseExpandRequest{ 719 StoreID: ulid.Make().String(), 720 ObjectType: "document", 721 Relation: "viewer", 722 User: &reverseexpand.UserRefObject{ 723 Object: &openfgav1.Object{ 724 Type: "user", 725 Id: "jon", 726 }, 727 }, 728 }, 729 model: ` 730 model 731 schema 1.1 732 733 type user 734 735 type group 736 relations 737 define member: [team#member] 738 739 type team 740 relations 741 define member: [user:*] 742 743 type document 744 relations 745 define viewer: [group#member]`, 746 tuples: []string{ 747 "team:tigers#member@user:*", 748 "group:eng#member@team:tigers#member", 749 "document:1#viewer@group:eng#member", 750 }, 751 expectedResult: []*reverseexpand.ReverseExpandResult{ 752 { 753 Object: "document:1", 754 ResultStatus: reverseexpand.NoFurtherEvalStatus, 755 }, 756 }, 757 expectedDSQueryCount: 3, 758 }, 759 { 760 name: "simple_typed_wildcard_and_direct_relation", 761 request: &reverseexpand.ReverseExpandRequest{ 762 StoreID: ulid.Make().String(), 763 ObjectType: "document", 764 Relation: "viewer", 765 User: &reverseexpand.UserRefObject{ 766 Object: &openfgav1.Object{Type: "user", Id: "jon"}, 767 }, 768 }, 769 model: ` 770 model 771 schema 1.1 772 773 type user 774 775 type document 776 relations 777 define viewer: [user, user:*]`, 778 tuples: []string{ 779 "document:1#viewer@user:*", 780 "document:2#viewer@user:jon", 781 }, 782 expectedResult: []*reverseexpand.ReverseExpandResult{ 783 { 784 Object: "document:1", 785 ResultStatus: reverseexpand.NoFurtherEvalStatus, 786 }, 787 { 788 Object: "document:2", 789 ResultStatus: reverseexpand.NoFurtherEvalStatus, 790 }, 791 }, 792 expectedDSQueryCount: 1, 793 }, 794 { 795 name: "simple_typed_wildcard_and_indirect_relation", 796 request: &reverseexpand.ReverseExpandRequest{ 797 StoreID: ulid.Make().String(), 798 ObjectType: "document", 799 Relation: "viewer", 800 User: &reverseexpand.UserRefObject{ 801 Object: &openfgav1.Object{ 802 Type: "user", 803 Id: "jon", 804 }, 805 }, 806 }, 807 model: ` 808 model 809 schema 1.1 810 811 type user 812 813 type group 814 relations 815 define member: [user, user:*] 816 817 type document 818 relations 819 define viewer: [group#member]`, 820 tuples: []string{ 821 "group:eng#member@user:*", 822 "group:fga#member@user:jon", 823 "document:1#viewer@group:eng#member", 824 "document:2#viewer@group:fga#member", 825 }, 826 expectedResult: []*reverseexpand.ReverseExpandResult{ 827 { 828 Object: "document:1", 829 ResultStatus: reverseexpand.NoFurtherEvalStatus, 830 }, 831 { 832 Object: "document:2", 833 ResultStatus: reverseexpand.NoFurtherEvalStatus, 834 }, 835 }, 836 expectedDSQueryCount: 3, 837 }, 838 { 839 name: "with_public_user_access_1", 840 request: &reverseexpand.ReverseExpandRequest{ 841 StoreID: ulid.Make().String(), 842 ObjectType: "document", 843 Relation: "viewer", 844 User: &reverseexpand.UserRefObject{ 845 Object: &openfgav1.Object{ 846 Type: "user", 847 Id: "*", 848 }, 849 }, 850 }, 851 model: ` 852 model 853 schema 1.1 854 855 type user 856 857 type group 858 relations 859 define member: [user:*] 860 861 type document 862 relations 863 define viewer: [group#member]`, 864 tuples: []string{ 865 "group:eng#member@user:*", 866 "group:other#member@employee:*", // assume this comes from a prior model 867 "document:1#viewer@group:eng#member", 868 "document:2#viewer@group:fga#member", 869 "document:3#viewer@group:other#member", 870 }, 871 expectedResult: []*reverseexpand.ReverseExpandResult{ 872 { 873 Object: "document:1", 874 ResultStatus: reverseexpand.NoFurtherEvalStatus, 875 }, 876 }, 877 expectedDSQueryCount: 2, 878 }, 879 { 880 name: "with_public_user_access_2", 881 request: &reverseexpand.ReverseExpandRequest{ 882 StoreID: ulid.Make().String(), 883 ObjectType: "resource", 884 Relation: "reader", 885 User: &reverseexpand.UserRefObject{ 886 Object: &openfgav1.Object{ 887 Type: "user", 888 Id: "bev", 889 }, 890 }, 891 }, 892 model: ` 893 model 894 schema 1.1 895 896 type user 897 898 type group 899 relations 900 define member: [user] 901 902 type resource 903 relations 904 define reader: [user, user:*, group#member] or writer 905 define writer: [user, user:*, group#member]`, 906 tuples: []string{ 907 "resource:x#writer@user:*", 908 }, 909 expectedResult: []*reverseexpand.ReverseExpandResult{ 910 { 911 Object: "resource:x", 912 ResultStatus: reverseexpand.NoFurtherEvalStatus, 913 }, 914 }, 915 expectedDSQueryCount: 3, 916 }, 917 { 918 name: "simple_typed_wildcard_with_contextual_tuples_1", 919 request: &reverseexpand.ReverseExpandRequest{ 920 StoreID: ulid.Make().String(), 921 ObjectType: "document", 922 Relation: "viewer", 923 User: &reverseexpand.UserRefObject{ 924 Object: &openfgav1.Object{Type: "user", Id: "jon"}, 925 }, 926 ContextualTuples: []*openfgav1.TupleKey{ 927 tuple.NewTupleKey("document:1", "viewer", "user:*"), 928 tuple.NewTupleKey("document:2", "viewer", "user:jon"), 929 }, 930 }, 931 model: ` 932 model 933 schema 1.1 934 935 type user 936 type document 937 relations 938 define viewer: [user, user:*]`, 939 expectedResult: []*reverseexpand.ReverseExpandResult{ 940 { 941 Object: "document:1", 942 ResultStatus: reverseexpand.NoFurtherEvalStatus, 943 }, 944 { 945 Object: "document:2", 946 ResultStatus: reverseexpand.NoFurtherEvalStatus, 947 }, 948 }, 949 expectedDSQueryCount: 1, 950 }, 951 { 952 name: "simple_typed_wildcard_with_contextual_tuples_2", 953 request: &reverseexpand.ReverseExpandRequest{ 954 StoreID: ulid.Make().String(), 955 ObjectType: "document", 956 Relation: "viewer", 957 User: &reverseexpand.UserRefTypedWildcard{Type: "user"}, 958 ContextualTuples: []*openfgav1.TupleKey{ 959 tuple.NewTupleKey("document:1", "viewer", "employee:*"), 960 tuple.NewTupleKey("document:2", "viewer", "user:*"), 961 }, 962 }, 963 model: ` 964 model 965 schema 1.1 966 967 type user 968 969 type employee 970 971 type document 972 relations 973 define viewer: [user:*]`, 974 expectedResult: []*reverseexpand.ReverseExpandResult{ 975 { 976 Object: "document:2", 977 ResultStatus: reverseexpand.NoFurtherEvalStatus, 978 }, 979 }, 980 expectedDSQueryCount: 1, 981 }, 982 { 983 name: "simple_typed_wildcard_with_contextual_tuples_3", 984 request: &reverseexpand.ReverseExpandRequest{ 985 StoreID: ulid.Make().String(), 986 ObjectType: "document", 987 Relation: "viewer", 988 User: &reverseexpand.UserRefObjectRelation{ 989 ObjectRelation: &openfgav1.ObjectRelation{ 990 Object: "group:eng", 991 Relation: "member", 992 }, 993 }, 994 ContextualTuples: []*openfgav1.TupleKey{ 995 tuple.NewTupleKey("document:1", "viewer", "group:eng#member"), 996 }, 997 }, 998 model: ` 999 model 1000 schema 1.1 1001 1002 type user 1003 1004 type group 1005 relations 1006 define member: [user] 1007 1008 type document 1009 relations 1010 define viewer: [group#member]`, 1011 expectedResult: []*reverseexpand.ReverseExpandResult{ 1012 { 1013 Object: "document:1", 1014 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1015 }, 1016 }, 1017 expectedDSQueryCount: 1, 1018 }, 1019 { 1020 name: "non-assignable_ttu_relationship", 1021 request: &reverseexpand.ReverseExpandRequest{ 1022 StoreID: ulid.Make().String(), 1023 ObjectType: "document", 1024 Relation: "viewer", 1025 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 1026 Type: "user", 1027 Id: "jon", 1028 }}, 1029 }, 1030 model: ` 1031 model 1032 schema 1.1 1033 1034 type user 1035 1036 type folder 1037 relations 1038 define viewer: [user, user:*] 1039 1040 type document 1041 relations 1042 define parent: [folder] 1043 define viewer: viewer from parent`, 1044 tuples: []string{ 1045 "document:1#parent@folder:1", 1046 "document:2#parent@folder:2", 1047 "folder:1#viewer@user:jon", 1048 "folder:2#viewer@user:*", 1049 }, 1050 expectedResult: []*reverseexpand.ReverseExpandResult{ 1051 { 1052 Object: "document:1", 1053 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1054 }, 1055 { 1056 Object: "document:2", 1057 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1058 }, 1059 }, 1060 expectedDSQueryCount: 3, 1061 }, 1062 { 1063 name: "non-assignable_ttu_relationship_without_wildcard_connectivity", 1064 request: &reverseexpand.ReverseExpandRequest{ 1065 StoreID: ulid.Make().String(), 1066 ObjectType: "document", 1067 Relation: "viewer", 1068 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 1069 Type: "user", 1070 Id: "jon", 1071 }}, 1072 }, 1073 model: ` 1074 model 1075 schema 1.1 1076 1077 type user 1078 type employee 1079 1080 type folder 1081 relations 1082 define viewer: [user, employee:*] 1083 1084 type document 1085 relations 1086 define parent: [folder] 1087 define viewer: viewer from parent`, 1088 tuples: []string{ 1089 "document:1#parent@folder:1", 1090 "document:2#parent@folder:2", 1091 "folder:1#viewer@user:jon", 1092 "folder:2#viewer@user:*", 1093 }, 1094 expectedResult: []*reverseexpand.ReverseExpandResult{ 1095 { 1096 Object: "document:1", 1097 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1098 }, 1099 }, 1100 expectedDSQueryCount: 2, 1101 }, 1102 { 1103 name: "non-assignable_ttu_relationship_through_indirection_1", 1104 request: &reverseexpand.ReverseExpandRequest{ 1105 StoreID: ulid.Make().String(), 1106 ObjectType: "document", 1107 Relation: "viewer", 1108 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 1109 Type: "user", 1110 Id: "jon", 1111 }}, 1112 }, 1113 model: ` 1114 model 1115 schema 1.1 1116 1117 type user 1118 1119 type group 1120 relations 1121 define member: [user:*] 1122 1123 type folder 1124 relations 1125 define viewer: [group#member] 1126 1127 type document 1128 relations 1129 define parent: [folder] 1130 define viewer: viewer from parent`, 1131 tuples: []string{ 1132 "document:1#parent@folder:1", 1133 "folder:1#viewer@group:eng#member", 1134 "group:eng#member@user:*", 1135 }, 1136 expectedResult: []*reverseexpand.ReverseExpandResult{ 1137 { 1138 Object: "document:1", 1139 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1140 }, 1141 }, 1142 expectedDSQueryCount: 3, 1143 }, 1144 { 1145 name: "non-assignable_ttu_relationship_through_indirection_2", 1146 request: &reverseexpand.ReverseExpandRequest{ 1147 StoreID: ulid.Make().String(), 1148 ObjectType: "resource", 1149 Relation: "writer", 1150 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 1151 Type: "user", 1152 Id: "anne", 1153 }}, 1154 }, 1155 model: ` 1156 model 1157 schema 1.1 1158 1159 type user 1160 1161 type org 1162 relations 1163 define dept: [group] 1164 define dept_member: member from dept 1165 1166 type group 1167 relations 1168 define member: [user] 1169 1170 type resource 1171 relations 1172 define writer: [org#dept_member]`, 1173 tuples: []string{ 1174 "resource:eng_handbook#writer@org:eng#dept_member", 1175 "org:eng#dept@group:fga", 1176 "group:fga#member@user:anne", 1177 }, 1178 expectedResult: []*reverseexpand.ReverseExpandResult{ 1179 { 1180 Object: "resource:eng_handbook", 1181 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1182 }, 1183 }, 1184 expectedDSQueryCount: 3, 1185 }, 1186 { 1187 name: "non-assignable_ttu_relationship_through_indirection_3", 1188 request: &reverseexpand.ReverseExpandRequest{ 1189 StoreID: ulid.Make().String(), 1190 ObjectType: "resource", 1191 Relation: "reader", 1192 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 1193 Type: "user", 1194 Id: "anne", 1195 }}, 1196 }, 1197 model: ` 1198 model 1199 schema 1.1 1200 1201 type user 1202 1203 type org 1204 relations 1205 define dept: [group] 1206 define dept_member: member from dept 1207 1208 type group 1209 relations 1210 define member: [user] 1211 1212 type resource 1213 relations 1214 define writer: [org#dept_member] 1215 define reader: [org#dept_member] or writer`, 1216 tuples: []string{ 1217 "resource:eng_handbook#writer@org:eng#dept_member", 1218 "org:eng#dept@group:fga", 1219 "group:fga#member@user:anne", 1220 }, 1221 expectedResult: []*reverseexpand.ReverseExpandResult{ 1222 { 1223 Object: "resource:eng_handbook", 1224 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1225 }, 1226 }, 1227 expectedDSQueryCount: 4, 1228 }, 1229 { 1230 name: "cyclical_tupleset_relation_terminates", 1231 request: &reverseexpand.ReverseExpandRequest{ 1232 StoreID: ulid.Make().String(), 1233 ObjectType: "node", 1234 Relation: "editor", 1235 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 1236 Type: "user", 1237 Id: "wonder", 1238 }}, 1239 }, 1240 model: ` 1241 model 1242 schema 1.1 1243 1244 type user 1245 1246 type node 1247 relations 1248 define parent: [node] 1249 define editor: [user] or editor from parent`, 1250 tuples: []string{ 1251 "node:abc#editor@user:wonder", 1252 }, 1253 expectedResult: []*reverseexpand.ReverseExpandResult{ 1254 { 1255 Object: "node:abc", 1256 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1257 }, 1258 }, 1259 expectedDSQueryCount: 2, 1260 }, 1261 { 1262 name: "does_not_send_duplicate_even_though_there_are_two_paths_to_same_solution", 1263 request: &reverseexpand.ReverseExpandRequest{ 1264 StoreID: ulid.Make().String(), 1265 ObjectType: "document", 1266 Relation: "viewer", 1267 User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{ 1268 Type: "user", 1269 Id: "jon", 1270 }}, 1271 }, 1272 model: ` 1273 model 1274 schema 1.1 1275 1276 type user 1277 1278 type group 1279 relations 1280 define member: [user] 1281 define maintainer: [user] 1282 1283 type document 1284 relations 1285 define viewer: [group#member,group#maintainer]`, 1286 tuples: []string{ 1287 "document:1#viewer@group:example1#maintainer", 1288 "group:example1#maintainer@user:jon", 1289 "group:example1#member@user:jon", 1290 }, 1291 expectedResult: []*reverseexpand.ReverseExpandResult{ 1292 { 1293 Object: "document:1", 1294 ResultStatus: reverseexpand.NoFurtherEvalStatus, 1295 }, 1296 }, 1297 expectedDSQueryCount: 4, 1298 }, 1299 } 1300 1301 for _, test := range tests { 1302 t.Run(test.name, func(t *testing.T) { 1303 storeID, model := storagetest.BootstrapFGAStore(t, ds, test.model, test.tuples) 1304 test.request.StoreID = storeID 1305 1306 var opts []reverseexpand.ReverseExpandQueryOption 1307 1308 if test.resolveNodeLimit != 0 { 1309 opts = append(opts, reverseexpand.WithResolveNodeLimit(test.resolveNodeLimit)) 1310 } 1311 1312 reverseExpandQuery := reverseexpand.NewReverseExpandQuery(ds, typesystem.New(model), opts...) 1313 1314 resultChan := make(chan *reverseexpand.ReverseExpandResult, 100) 1315 1316 timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 1317 defer cancel() 1318 1319 resolutionMetadata := reverseexpand.NewResolutionMetadata() 1320 1321 reverseExpandErrCh := make(chan error, 1) 1322 go func() { 1323 errReverseExpand := reverseExpandQuery.Execute(timeoutCtx, test.request, resultChan, resolutionMetadata) 1324 if errReverseExpand != nil { 1325 reverseExpandErrCh <- errReverseExpand 1326 } 1327 }() 1328 1329 var results []*reverseexpand.ReverseExpandResult 1330 1331 for { 1332 select { 1333 case errFromChannel := <-reverseExpandErrCh: 1334 if errors.Is(errFromChannel, context.DeadlineExceeded) { 1335 require.FailNow(t, "unexpected timeout") 1336 } 1337 require.ErrorIs(t, errFromChannel, test.expectedError) 1338 return 1339 case res, channelOpen := <-resultChan: 1340 if !channelOpen { 1341 t.Log("channel closed") 1342 if test.expectedError == nil { 1343 require.ElementsMatch(t, test.expectedResult, results) 1344 require.Equal(t, test.expectedDSQueryCount, *resolutionMetadata.DatastoreQueryCount) 1345 } else { 1346 require.FailNow(t, "expected an error, got none") 1347 } 1348 return 1349 } else { 1350 t.Logf("appending result %s", res.Object) 1351 results = append(results, res) 1352 } 1353 } 1354 } 1355 }) 1356 } 1357 }