github.com/openfga/openfga@v1.5.4-rc1/pkg/server/test/expand.go (about) 1 package test 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/oklog/ulid/v2" 10 openfgav1 "github.com/openfga/api/proto/openfga/v1" 11 "github.com/stretchr/testify/require" 12 "google.golang.org/protobuf/testing/protocmp" 13 14 "github.com/openfga/openfga/pkg/server/commands" 15 serverErrors "github.com/openfga/openfga/pkg/server/errors" 16 "github.com/openfga/openfga/pkg/storage" 17 "github.com/openfga/openfga/pkg/testutils" 18 "github.com/openfga/openfga/pkg/tuple" 19 "github.com/openfga/openfga/pkg/typesystem" 20 ) 21 22 func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) { 23 tests := []struct { 24 name string 25 model *openfgav1.AuthorizationModel 26 tuples []*openfgav1.TupleKey 27 request *openfgav1.ExpandRequest 28 expected *openfgav1.ExpandResponse 29 }{ 30 { 31 name: "1.1_simple_direct", 32 model: testutils.MustTransformDSLToProtoWithID(` 33 model 34 schema 1.1 35 type user 36 type repo 37 relations 38 define admin: [user]`), 39 tuples: []*openfgav1.TupleKey{ 40 { 41 Object: "repo:openfga/foo", 42 Relation: "admin", 43 User: "user:jon", 44 }, 45 }, 46 request: &openfgav1.ExpandRequest{ 47 TupleKey: tuple.NewExpandRequestTupleKey( 48 "repo:openfga/foo", 49 "admin", 50 ), 51 }, 52 expected: &openfgav1.ExpandResponse{ 53 Tree: &openfgav1.UsersetTree{ 54 Root: &openfgav1.UsersetTree_Node{ 55 Name: "repo:openfga/foo#admin", 56 Value: &openfgav1.UsersetTree_Node_Leaf{ 57 Leaf: &openfgav1.UsersetTree_Leaf{ 58 Value: &openfgav1.UsersetTree_Leaf_Users{ 59 Users: &openfgav1.UsersetTree_Users{ 60 Users: []string{"user:jon"}, 61 }, 62 }, 63 }, 64 }, 65 }, 66 }, 67 }, 68 }, 69 { 70 name: "1.1_computed_userset", 71 model: testutils.MustTransformDSLToProtoWithID(` 72 model 73 schema 1.1 74 type user 75 type repo 76 relations 77 define admin: [user] 78 define writer: admin`), 79 tuples: []*openfgav1.TupleKey{}, 80 request: &openfgav1.ExpandRequest{ 81 TupleKey: tuple.NewExpandRequestTupleKey( 82 "repo:openfga/foo", 83 "writer", 84 ), 85 }, 86 expected: &openfgav1.ExpandResponse{ 87 Tree: &openfgav1.UsersetTree{ 88 Root: &openfgav1.UsersetTree_Node{ 89 Name: "repo:openfga/foo#writer", 90 Value: &openfgav1.UsersetTree_Node_Leaf{ 91 Leaf: &openfgav1.UsersetTree_Leaf{ 92 Value: &openfgav1.UsersetTree_Leaf_Computed{ 93 Computed: &openfgav1.UsersetTree_Computed{ 94 Userset: "repo:openfga/foo#admin", 95 }, 96 }, 97 }, 98 }, 99 }, 100 }, 101 }, 102 }, 103 { 104 name: "1.1_tuple_to_userset", 105 model: testutils.MustTransformDSLToProtoWithID(` 106 model 107 schema 1.1 108 type user 109 type repo 110 relations 111 define admin: repo_admin from manager 112 define manager: [org] 113 type org 114 relations 115 define repo_admin: [user]`), 116 tuples: []*openfgav1.TupleKey{ 117 { 118 Object: "repo:openfga/foo", 119 Relation: "manager", 120 User: "org:openfga", 121 }, 122 { 123 Object: "org:openfga", 124 Relation: "repo_admin", 125 User: "user:jon", 126 }, 127 }, 128 request: &openfgav1.ExpandRequest{ 129 TupleKey: tuple.NewExpandRequestTupleKey( 130 "repo:openfga/foo", 131 "admin", 132 ), 133 }, 134 expected: &openfgav1.ExpandResponse{ 135 Tree: &openfgav1.UsersetTree{ 136 Root: &openfgav1.UsersetTree_Node{ 137 Name: "repo:openfga/foo#admin", 138 Value: &openfgav1.UsersetTree_Node_Leaf{ 139 Leaf: &openfgav1.UsersetTree_Leaf{ 140 Value: &openfgav1.UsersetTree_Leaf_TupleToUserset{ 141 TupleToUserset: &openfgav1.UsersetTree_TupleToUserset{ 142 Tupleset: "repo:openfga/foo#manager", 143 Computed: []*openfgav1.UsersetTree_Computed{ 144 { 145 Userset: "org:openfga#repo_admin", 146 }, 147 }, 148 }, 149 }, 150 }, 151 }, 152 }, 153 }, 154 }, 155 }, 156 { 157 name: "1.1_tuple_to_userset_II", 158 model: testutils.MustTransformDSLToProtoWithID(` 159 model 160 schema 1.1 161 type user 162 type repo 163 relations 164 define admin: repo_admin from manager 165 define manager: [org] 166 type org 167 relations 168 define repo_admin: [user]`), 169 tuples: []*openfgav1.TupleKey{ 170 { 171 Object: "repo:openfga/foo", 172 Relation: "manager", 173 User: "org:openfga", 174 }, 175 { 176 Object: "org:openfga", 177 Relation: "repo_admin", 178 User: "user:jon", 179 }, 180 { 181 Object: "repo:openfga/foo", 182 Relation: "manager", 183 User: "amy", // should be skipped since it's not a valid target for a tupleset relation 184 }, 185 }, 186 request: &openfgav1.ExpandRequest{ 187 TupleKey: tuple.NewExpandRequestTupleKey( 188 "repo:openfga/foo", 189 "admin", 190 ), 191 }, 192 expected: &openfgav1.ExpandResponse{ 193 Tree: &openfgav1.UsersetTree{ 194 Root: &openfgav1.UsersetTree_Node{ 195 Name: "repo:openfga/foo#admin", 196 Value: &openfgav1.UsersetTree_Node_Leaf{ 197 Leaf: &openfgav1.UsersetTree_Leaf{ 198 Value: &openfgav1.UsersetTree_Leaf_TupleToUserset{ 199 TupleToUserset: &openfgav1.UsersetTree_TupleToUserset{ 200 Tupleset: "repo:openfga/foo#manager", 201 Computed: []*openfgav1.UsersetTree_Computed{ 202 { 203 Userset: "org:openfga#repo_admin", 204 }, 205 }, 206 }, 207 }, 208 }, 209 }, 210 }, 211 }, 212 }, 213 }, 214 { 215 name: "1.1_tuple_to_userset_implicit", 216 model: testutils.MustTransformDSLToProtoWithID(` 217 model 218 schema 1.1 219 type user 220 type repo 221 relations 222 define admin: repo_admin from manager 223 define manager: [org] 224 type org 225 relations 226 define repo_admin: [user]`), 227 tuples: []*openfgav1.TupleKey{ 228 { 229 Object: "repo:openfga/foo", 230 Relation: "manager", 231 User: "org:openfga", 232 }, 233 { 234 Object: "org:openfga", 235 Relation: "repo_admin", 236 User: "user:jon", 237 }, 238 }, 239 request: &openfgav1.ExpandRequest{ 240 TupleKey: tuple.NewExpandRequestTupleKey( 241 "repo:openfga/foo", 242 "admin", 243 ), 244 }, 245 expected: &openfgav1.ExpandResponse{ 246 Tree: &openfgav1.UsersetTree{ 247 Root: &openfgav1.UsersetTree_Node{ 248 Name: "repo:openfga/foo#admin", 249 Value: &openfgav1.UsersetTree_Node_Leaf{ 250 Leaf: &openfgav1.UsersetTree_Leaf{ 251 Value: &openfgav1.UsersetTree_Leaf_TupleToUserset{ 252 TupleToUserset: &openfgav1.UsersetTree_TupleToUserset{ 253 Tupleset: "repo:openfga/foo#manager", 254 Computed: []*openfgav1.UsersetTree_Computed{ 255 { 256 Userset: "org:openfga#repo_admin", 257 }, 258 }, 259 }, 260 }, 261 }, 262 }, 263 }, 264 }, 265 }, 266 }, 267 { 268 name: "1.1_simple_union", 269 model: testutils.MustTransformDSLToProtoWithID(` 270 model 271 schema 1.1 272 type user 273 type repo 274 relations 275 define admin: [user] 276 define writer: [user] or admin 277 type org 278 relations 279 define repo_admin: [user]`), 280 tuples: []*openfgav1.TupleKey{ 281 { 282 Object: "repo:openfga/foo", 283 Relation: "writer", 284 User: "user:jon", 285 }, 286 }, 287 request: &openfgav1.ExpandRequest{ 288 TupleKey: tuple.NewExpandRequestTupleKey( 289 "repo:openfga/foo", 290 "writer", 291 ), 292 }, 293 expected: &openfgav1.ExpandResponse{ 294 Tree: &openfgav1.UsersetTree{ 295 Root: &openfgav1.UsersetTree_Node{ 296 Name: "repo:openfga/foo#writer", 297 Value: &openfgav1.UsersetTree_Node_Union{ 298 Union: &openfgav1.UsersetTree_Nodes{ 299 Nodes: []*openfgav1.UsersetTree_Node{ 300 { 301 Name: "repo:openfga/foo#writer", 302 Value: &openfgav1.UsersetTree_Node_Leaf{ 303 Leaf: &openfgav1.UsersetTree_Leaf{ 304 Value: &openfgav1.UsersetTree_Leaf_Users{ 305 Users: &openfgav1.UsersetTree_Users{ 306 Users: []string{"user:jon"}, 307 }, 308 }, 309 }, 310 }, 311 }, 312 { 313 Name: "repo:openfga/foo#writer", 314 Value: &openfgav1.UsersetTree_Node_Leaf{ 315 Leaf: &openfgav1.UsersetTree_Leaf{ 316 Value: &openfgav1.UsersetTree_Leaf_Computed{ 317 Computed: &openfgav1.UsersetTree_Computed{ 318 Userset: "repo:openfga/foo#admin", 319 }, 320 }, 321 }, 322 }, 323 }, 324 }, 325 }, 326 }, 327 }, 328 }, 329 }, 330 }, 331 { 332 name: "1.1_simple_difference", 333 model: testutils.MustTransformDSLToProtoWithID(` 334 model 335 schema 1.1 336 type user 337 type repo 338 relations 339 define admin: [user] 340 define banned: [user] 341 define active_admin: admin but not banned`), 342 tuples: []*openfgav1.TupleKey{}, 343 request: &openfgav1.ExpandRequest{ 344 TupleKey: tuple.NewExpandRequestTupleKey( 345 "repo:openfga/foo", 346 "active_admin", 347 ), 348 }, 349 expected: &openfgav1.ExpandResponse{ 350 Tree: &openfgav1.UsersetTree{ 351 Root: &openfgav1.UsersetTree_Node{ 352 Name: "repo:openfga/foo#active_admin", 353 Value: &openfgav1.UsersetTree_Node_Difference{ 354 Difference: &openfgav1.UsersetTree_Difference{ 355 Base: &openfgav1.UsersetTree_Node{ 356 Name: "repo:openfga/foo#active_admin", 357 Value: &openfgav1.UsersetTree_Node_Leaf{ 358 Leaf: &openfgav1.UsersetTree_Leaf{ 359 Value: &openfgav1.UsersetTree_Leaf_Computed{ 360 Computed: &openfgav1.UsersetTree_Computed{ 361 Userset: "repo:openfga/foo#admin", 362 }, 363 }, 364 }, 365 }, 366 }, 367 Subtract: &openfgav1.UsersetTree_Node{ 368 Name: "repo:openfga/foo#active_admin", 369 Value: &openfgav1.UsersetTree_Node_Leaf{ 370 Leaf: &openfgav1.UsersetTree_Leaf{ 371 Value: &openfgav1.UsersetTree_Leaf_Computed{ 372 Computed: &openfgav1.UsersetTree_Computed{ 373 Userset: "repo:openfga/foo#banned", 374 }, 375 }, 376 }, 377 }, 378 }, 379 }, 380 }, 381 }, 382 }, 383 }, 384 }, 385 { 386 name: "1.1_intersection", 387 model: testutils.MustTransformDSLToProtoWithID(` 388 model 389 schema 1.1 390 type user 391 type repo 392 relations 393 define admin: [user] 394 define writer: [user] and admin`), 395 tuples: []*openfgav1.TupleKey{}, 396 request: &openfgav1.ExpandRequest{ 397 TupleKey: tuple.NewExpandRequestTupleKey( 398 "repo:openfga/foo", 399 "writer", 400 ), 401 }, 402 expected: &openfgav1.ExpandResponse{ 403 Tree: &openfgav1.UsersetTree{ 404 Root: &openfgav1.UsersetTree_Node{ 405 Name: "repo:openfga/foo#writer", 406 Value: &openfgav1.UsersetTree_Node_Intersection{ 407 Intersection: &openfgav1.UsersetTree_Nodes{ 408 Nodes: []*openfgav1.UsersetTree_Node{ 409 { 410 Name: "repo:openfga/foo#writer", 411 Value: &openfgav1.UsersetTree_Node_Leaf{ 412 Leaf: &openfgav1.UsersetTree_Leaf{ 413 Value: &openfgav1.UsersetTree_Leaf_Users{ 414 Users: &openfgav1.UsersetTree_Users{ 415 Users: []string{}, 416 }, 417 }, 418 }, 419 }, 420 }, 421 { 422 Name: "repo:openfga/foo#writer", 423 Value: &openfgav1.UsersetTree_Node_Leaf{ 424 Leaf: &openfgav1.UsersetTree_Leaf{ 425 Value: &openfgav1.UsersetTree_Leaf_Computed{ 426 Computed: &openfgav1.UsersetTree_Computed{ 427 Userset: "repo:openfga/foo#admin", 428 }, 429 }, 430 }, 431 }, 432 }, 433 }, 434 }, 435 }, 436 }, 437 }, 438 }, 439 }, 440 { 441 name: "1.1_complex_tree", 442 model: testutils.MustTransformDSLToProtoWithID(` 443 model 444 schema 1.1 445 type user 446 type repo 447 relations 448 define admin: [user] 449 define owner: [org] 450 define banned_writer: [user] 451 define writer: ([user] or repo_writer from owner) but not banned_writer 452 type org 453 relations 454 define repo_writer: [user]`), 455 tuples: []*openfgav1.TupleKey{ 456 { 457 Object: "repo:openfga/foo", 458 Relation: "owner", 459 User: "org:openfga", 460 }, 461 { 462 Object: "repo:openfga/foo", 463 Relation: "writer", 464 User: "user:jon", 465 }, 466 }, 467 request: &openfgav1.ExpandRequest{ 468 TupleKey: tuple.NewExpandRequestTupleKey( 469 "repo:openfga/foo", 470 "writer", 471 ), 472 }, 473 expected: &openfgav1.ExpandResponse{ 474 Tree: &openfgav1.UsersetTree{ 475 Root: &openfgav1.UsersetTree_Node{ 476 Name: "repo:openfga/foo#writer", 477 Value: &openfgav1.UsersetTree_Node_Difference{ 478 Difference: &openfgav1.UsersetTree_Difference{ 479 Base: &openfgav1.UsersetTree_Node{ 480 Name: "repo:openfga/foo#writer", 481 Value: &openfgav1.UsersetTree_Node_Union{ 482 Union: &openfgav1.UsersetTree_Nodes{ 483 Nodes: []*openfgav1.UsersetTree_Node{ 484 { 485 Name: "repo:openfga/foo#writer", 486 Value: &openfgav1.UsersetTree_Node_Leaf{ 487 Leaf: &openfgav1.UsersetTree_Leaf{ 488 Value: &openfgav1.UsersetTree_Leaf_Users{ 489 Users: &openfgav1.UsersetTree_Users{ 490 Users: []string{"user:jon"}, 491 }, 492 }, 493 }, 494 }, 495 }, 496 { 497 Name: "repo:openfga/foo#writer", 498 Value: &openfgav1.UsersetTree_Node_Leaf{ 499 Leaf: &openfgav1.UsersetTree_Leaf{ 500 Value: &openfgav1.UsersetTree_Leaf_TupleToUserset{ 501 TupleToUserset: &openfgav1.UsersetTree_TupleToUserset{ 502 Tupleset: "repo:openfga/foo#owner", 503 Computed: []*openfgav1.UsersetTree_Computed{ 504 {Userset: "org:openfga#repo_writer"}, 505 }, 506 }, 507 }, 508 }, 509 }, 510 }, 511 }, 512 }, 513 }, 514 }, 515 Subtract: &openfgav1.UsersetTree_Node{ 516 Name: "repo:openfga/foo#writer", 517 Value: &openfgav1.UsersetTree_Node_Leaf{ 518 Leaf: &openfgav1.UsersetTree_Leaf{ 519 Value: &openfgav1.UsersetTree_Leaf_Computed{ 520 Computed: &openfgav1.UsersetTree_Computed{ 521 Userset: "repo:openfga/foo#banned_writer", 522 }, 523 }, 524 }, 525 }, 526 }, 527 }, 528 }, 529 }, 530 }, 531 }, 532 }, 533 { 534 name: "1.1_Tuple_involving_userset_that_is_not_involved_in_TTU_rewrite", 535 model: testutils.MustTransformDSLToProtoWithID(` 536 model 537 schema 1.1 538 type user 539 type document 540 relations 541 define parent: [document#editor] 542 define editor: [user]`), 543 tuples: []*openfgav1.TupleKey{ 544 tuple.NewTupleKey("document:1", "parent", "document:2#editor"), 545 }, 546 request: &openfgav1.ExpandRequest{ 547 TupleKey: tuple.NewExpandRequestTupleKey("document:1", "parent"), 548 }, 549 expected: &openfgav1.ExpandResponse{ 550 Tree: &openfgav1.UsersetTree{ 551 Root: &openfgav1.UsersetTree_Node{ 552 Name: "document:1#parent", 553 Value: &openfgav1.UsersetTree_Node_Leaf{ 554 Leaf: &openfgav1.UsersetTree_Leaf{ 555 Value: &openfgav1.UsersetTree_Leaf_Users{ 556 Users: &openfgav1.UsersetTree_Users{ 557 Users: []string{"document:2#editor"}, 558 }, 559 }, 560 }, 561 }, 562 }, 563 }, 564 }, 565 }, 566 { 567 name: "self_defined_userset_not_returned", 568 model: testutils.MustTransformDSLToProtoWithID(` 569 model 570 schema 1.1 571 type user 572 type group 573 relations 574 define viewer: [user] 575 `), 576 tuples: []*openfgav1.TupleKey{}, 577 request: &openfgav1.ExpandRequest{ 578 TupleKey: tuple.NewExpandRequestTupleKey("group:1", "viewer"), 579 }, 580 expected: &openfgav1.ExpandResponse{ 581 Tree: &openfgav1.UsersetTree{ 582 Root: &openfgav1.UsersetTree_Node{ 583 Name: "group:1#viewer", 584 Value: &openfgav1.UsersetTree_Node_Leaf{ 585 Leaf: &openfgav1.UsersetTree_Leaf{ 586 Value: &openfgav1.UsersetTree_Leaf_Users{ 587 Users: &openfgav1.UsersetTree_Users{ 588 // group:1#viewer isn't included because it's implicit 589 Users: []string{}, 590 }, 591 }, 592 }, 593 }, 594 }, 595 }, 596 }, 597 }, 598 { 599 name: "self_defined_userset_not_returned_even_if_tuple_written", 600 model: testutils.MustTransformDSLToProtoWithID(` 601 model 602 schema 1.1 603 type user 604 type group 605 relations 606 define viewer: [user] 607 `), 608 tuples: []*openfgav1.TupleKey{ 609 tuple.NewTupleKey("group:1", "viewer", "group:1#viewer"), // invalid, so should be skipped over 610 }, 611 request: &openfgav1.ExpandRequest{ 612 TupleKey: tuple.NewExpandRequestTupleKey("group:1", "viewer"), 613 }, 614 expected: &openfgav1.ExpandResponse{ 615 Tree: &openfgav1.UsersetTree{ 616 Root: &openfgav1.UsersetTree_Node{ 617 Name: "group:1#viewer", 618 Value: &openfgav1.UsersetTree_Node_Leaf{ 619 Leaf: &openfgav1.UsersetTree_Leaf{ 620 Value: &openfgav1.UsersetTree_Leaf_Users{ 621 Users: &openfgav1.UsersetTree_Users{}, 622 }, 623 }, 624 }, 625 }, 626 }, 627 }, 628 }, 629 { 630 name: "self_defined_userset_returned_if_tuple_written", 631 model: testutils.MustTransformDSLToProtoWithID(` 632 model 633 schema 1.1 634 type user 635 type group 636 relations 637 define viewer: [user, group#viewer] 638 `), 639 tuples: []*openfgav1.TupleKey{ 640 tuple.NewTupleKey("group:1", "viewer", "group:1#viewer"), 641 }, 642 request: &openfgav1.ExpandRequest{ 643 TupleKey: tuple.NewExpandRequestTupleKey("group:1", "viewer"), 644 }, 645 expected: &openfgav1.ExpandResponse{ 646 Tree: &openfgav1.UsersetTree{ 647 Root: &openfgav1.UsersetTree_Node{ 648 Name: "group:1#viewer", 649 Value: &openfgav1.UsersetTree_Node_Leaf{ 650 Leaf: &openfgav1.UsersetTree_Leaf{ 651 Value: &openfgav1.UsersetTree_Leaf_Users{ 652 Users: &openfgav1.UsersetTree_Users{ 653 Users: []string{"group:1#viewer"}, 654 }, 655 }, 656 }, 657 }, 658 }, 659 }, 660 }, 661 }, 662 { 663 name: "cyclical_tuples", 664 model: testutils.MustTransformDSLToProtoWithID(` 665 model 666 schema 1.1 667 type user 668 type group 669 relations 670 define viewer: [user, group#viewer] 671 `), 672 tuples: []*openfgav1.TupleKey{ 673 tuple.NewTupleKey("group:2", "viewer", "group:3#viewer"), 674 tuple.NewTupleKey("group:1", "viewer", "group:2#viewer"), 675 tuple.NewTupleKey("group:3", "viewer", "group:1#viewer"), 676 }, 677 request: &openfgav1.ExpandRequest{ 678 TupleKey: tuple.NewExpandRequestTupleKey("group:1", "viewer"), 679 }, 680 expected: &openfgav1.ExpandResponse{ 681 Tree: &openfgav1.UsersetTree{ 682 Root: &openfgav1.UsersetTree_Node{ 683 Name: "group:1#viewer", 684 Value: &openfgav1.UsersetTree_Node_Leaf{ 685 Leaf: &openfgav1.UsersetTree_Leaf{ 686 Value: &openfgav1.UsersetTree_Leaf_Users{ 687 Users: &openfgav1.UsersetTree_Users{ 688 Users: []string{"group:2#viewer"}, 689 }, 690 }, 691 }, 692 }, 693 }, 694 }, 695 }, 696 }, 697 { 698 name: "nested_groups_I", 699 model: testutils.MustTransformDSLToProtoWithID(` 700 model 701 schema 1.1 702 703 type employee 704 relations 705 define can_manage: manager or can_manage from manager 706 define manager: [employee] 707 708 type report 709 relations 710 define approver: can_manage from submitter 711 define submitter: [employee] 712 `), 713 tuples: []*openfgav1.TupleKey{ 714 // employee:d has no manager 715 tuple.NewTupleKey("employee:c", "manager", "employee:d"), 716 tuple.NewTupleKey("employee:b", "manager", "employee:c"), 717 tuple.NewTupleKey("employee:a", "manager", "employee:b"), 718 }, 719 request: &openfgav1.ExpandRequest{ 720 TupleKey: tuple.NewExpandRequestTupleKey("employee:d", "manager"), 721 }, 722 expected: &openfgav1.ExpandResponse{ 723 Tree: &openfgav1.UsersetTree{ 724 Root: &openfgav1.UsersetTree_Node{ 725 Name: "employee:d#manager", 726 Value: &openfgav1.UsersetTree_Node_Leaf{ 727 Leaf: &openfgav1.UsersetTree_Leaf{ 728 Value: &openfgav1.UsersetTree_Leaf_Users{ 729 Users: &openfgav1.UsersetTree_Users{ 730 // employee:d has no manager 731 Users: []string{}, 732 }, 733 }, 734 }, 735 }, 736 }, 737 }, 738 }, 739 }, 740 { 741 name: "nested_groups_II", 742 model: testutils.MustTransformDSLToProtoWithID(` 743 model 744 schema 1.1 745 746 type employee 747 relations 748 define can_manage: manager or can_manage from manager 749 define manager: [employee] 750 751 type report 752 relations 753 define approver: can_manage from submitter 754 define submitter: [employee] 755 `), 756 tuples: []*openfgav1.TupleKey{ 757 tuple.NewTupleKey("employee:c", "manager", "employee:d"), 758 tuple.NewTupleKey("employee:b", "manager", "employee:c"), 759 tuple.NewTupleKey("employee:a", "manager", "employee:b"), 760 }, 761 request: &openfgav1.ExpandRequest{ 762 TupleKey: tuple.NewExpandRequestTupleKey("employee:c", "manager"), 763 }, 764 expected: &openfgav1.ExpandResponse{ 765 Tree: &openfgav1.UsersetTree{ 766 Root: &openfgav1.UsersetTree_Node{ 767 Name: "employee:c#manager", 768 Value: &openfgav1.UsersetTree_Node_Leaf{ 769 Leaf: &openfgav1.UsersetTree_Leaf{ 770 Value: &openfgav1.UsersetTree_Leaf_Users{ 771 Users: &openfgav1.UsersetTree_Users{ 772 Users: []string{"employee:d"}, 773 }, 774 }, 775 }, 776 }, 777 }, 778 }, 779 }, 780 }, 781 } 782 783 ctx := context.Background() 784 785 for _, test := range tests { 786 t.Run(test.name, func(t *testing.T) { 787 // arrange 788 store := ulid.Make().String() 789 err := datastore.WriteAuthorizationModel(ctx, store, test.model) 790 require.NoError(t, err) 791 792 err = datastore.Write( 793 ctx, 794 store, 795 []*openfgav1.TupleKeyWithoutCondition{}, 796 test.tuples, 797 ) 798 require.NoError(t, err) 799 800 require.NoError(t, err) 801 test.request.StoreId = store 802 test.request.AuthorizationModelId = test.model.GetId() 803 804 // act 805 query := commands.NewExpandQuery(datastore) 806 got, err := query.Execute(ctx, test.request) 807 require.NoError(t, err) 808 809 // assert 810 if diff := cmp.Diff(test.expected, got, protocmp.Transform()); diff != "" { 811 t.Errorf("mismatch (-want, +got):\n%s", diff) 812 } 813 }) 814 } 815 } 816 817 func TestExpandQueryErrors(t *testing.T, datastore storage.OpenFGADatastore) { 818 tests := []struct { 819 name string 820 model *openfgav1.AuthorizationModel 821 tuples []*openfgav1.TupleKey 822 request *openfgav1.ExpandRequest 823 allowSchema10 bool 824 expected error 825 }{ 826 { 827 name: "missing_object_in_request", 828 request: &openfgav1.ExpandRequest{ 829 TupleKey: &openfgav1.ExpandRequestTupleKey{ 830 Relation: "bar", 831 }, 832 }, 833 model: &openfgav1.AuthorizationModel{ 834 Id: ulid.Make().String(), 835 SchemaVersion: typesystem.SchemaVersion1_1, 836 TypeDefinitions: []*openfgav1.TypeDefinition{ 837 {Type: "repo"}, 838 }, 839 }, 840 allowSchema10: true, 841 expected: serverErrors.InvalidExpandInput, 842 }, 843 { 844 name: "missing_object_id_and_type_in_request", 845 request: &openfgav1.ExpandRequest{ 846 TupleKey: &openfgav1.ExpandRequestTupleKey{ 847 Object: ":", 848 Relation: "bar", 849 }, 850 }, 851 model: &openfgav1.AuthorizationModel{ 852 Id: ulid.Make().String(), 853 SchemaVersion: typesystem.SchemaVersion1_1, 854 TypeDefinitions: []*openfgav1.TypeDefinition{ 855 {Type: "repo"}, 856 }, 857 }, 858 allowSchema10: true, 859 expected: serverErrors.ValidationError( 860 fmt.Errorf("invalid 'object' field format"), 861 ), 862 }, 863 { 864 name: "missing_object_id_in_request", 865 request: &openfgav1.ExpandRequest{ 866 TupleKey: &openfgav1.ExpandRequestTupleKey{ 867 Object: "github:", 868 Relation: "bar", 869 }, 870 }, 871 model: &openfgav1.AuthorizationModel{ 872 Id: ulid.Make().String(), 873 SchemaVersion: typesystem.SchemaVersion1_1, 874 TypeDefinitions: []*openfgav1.TypeDefinition{ 875 {Type: "repo"}, 876 }, 877 }, 878 allowSchema10: true, 879 expected: serverErrors.ValidationError( 880 fmt.Errorf("invalid 'object' field format"), 881 ), 882 }, 883 { 884 name: "missing_relation_in_request", 885 request: &openfgav1.ExpandRequest{ 886 TupleKey: &openfgav1.ExpandRequestTupleKey{ 887 Object: "bar", 888 }, 889 }, 890 model: &openfgav1.AuthorizationModel{ 891 Id: ulid.Make().String(), 892 SchemaVersion: typesystem.SchemaVersion1_1, 893 TypeDefinitions: []*openfgav1.TypeDefinition{ 894 {Type: "repo"}, 895 }, 896 }, 897 allowSchema10: true, 898 expected: serverErrors.InvalidExpandInput, 899 }, 900 { 901 name: "1.1_object_type_not_found_in_model", 902 request: &openfgav1.ExpandRequest{ 903 TupleKey: &openfgav1.ExpandRequestTupleKey{ 904 Object: "foo:bar", 905 Relation: "baz", 906 }, 907 }, 908 model: &openfgav1.AuthorizationModel{ 909 Id: ulid.Make().String(), 910 SchemaVersion: typesystem.SchemaVersion1_1, 911 TypeDefinitions: []*openfgav1.TypeDefinition{ 912 {Type: "repo"}, 913 }, 914 }, 915 allowSchema10: true, 916 expected: serverErrors.ValidationError( 917 &tuple.TypeNotFoundError{TypeName: "foo"}, 918 ), 919 }, 920 { 921 name: "1.1_relation_not_found_in_model", 922 model: &openfgav1.AuthorizationModel{ 923 Id: ulid.Make().String(), 924 SchemaVersion: typesystem.SchemaVersion1_1, 925 TypeDefinitions: []*openfgav1.TypeDefinition{ 926 {Type: "repo"}, 927 }, 928 }, 929 request: &openfgav1.ExpandRequest{ 930 TupleKey: &openfgav1.ExpandRequestTupleKey{ 931 Object: "repo:bar", 932 Relation: "baz", 933 }, 934 }, 935 allowSchema10: true, 936 expected: serverErrors.ValidationError( 937 &tuple.RelationNotFoundError{ 938 TypeName: "repo", 939 Relation: "baz", 940 }, 941 ), 942 }, 943 } 944 945 ctx := context.Background() 946 947 for _, test := range tests { 948 t.Run(test.name, func(t *testing.T) { 949 // arrange 950 store := ulid.Make().String() 951 err := datastore.WriteAuthorizationModel(ctx, store, test.model) 952 require.NoError(t, err) 953 954 err = datastore.Write( 955 ctx, 956 store, 957 []*openfgav1.TupleKeyWithoutCondition{}, 958 test.tuples, 959 ) 960 require.NoError(t, err) 961 962 require.NoError(t, err) 963 test.request.StoreId = store 964 test.request.AuthorizationModelId = test.model.GetId() 965 966 // act 967 query := commands.NewExpandQuery(datastore) 968 resp, err := query.Execute(ctx, test.request) 969 970 // assert 971 require.Nil(t, resp) 972 require.ErrorIs(t, err, test.expected) 973 }) 974 } 975 }