github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/computed/computecheck_test.go (about) 1 package computed_test 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 "google.golang.org/protobuf/types/known/structpb" 9 10 "github.com/authzed/spicedb/internal/datastore/memdb" 11 "github.com/authzed/spicedb/internal/dispatch/graph" 12 "github.com/authzed/spicedb/internal/graph/computed" 13 log "github.com/authzed/spicedb/internal/logging" 14 datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" 15 "github.com/authzed/spicedb/pkg/caveats/types" 16 "github.com/authzed/spicedb/pkg/datastore" 17 core "github.com/authzed/spicedb/pkg/proto/core/v1" 18 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 19 "github.com/authzed/spicedb/pkg/schemadsl/compiler" 20 "github.com/authzed/spicedb/pkg/tuple" 21 22 "github.com/stretchr/testify/require" 23 ) 24 25 type caveatedUpdate struct { 26 Operation core.RelationTupleUpdate_Operation 27 tuple string 28 caveatName string 29 context map[string]any 30 } 31 32 func TestComputeCheckWithCaveats(t *testing.T) { 33 type check struct { 34 check string 35 context map[string]any 36 member v1.ResourceCheckResult_Membership 37 expectedMissingFields []string 38 error string 39 } 40 41 testCases := []struct { 42 name string 43 schema string 44 updates []caveatedUpdate 45 checks []check 46 }{ 47 { 48 "simple test", 49 `definition user {} 50 51 definition organization { 52 relation admin: user | user with testcaveat 53 } 54 55 definition document { 56 relation org: organization | organization with anothercaveat 57 relation viewer: user | user with testcaveat 58 relation editor: user | user with testcaveat 59 60 permission edit = editor + org->admin 61 permission view = viewer + edit 62 } 63 64 caveat testcaveat(somecondition int, somebool bool) { 65 somecondition == 42 && somebool 66 } 67 68 caveat anothercaveat(anothercondition uint) { 69 anothercondition == 15 70 } 71 `, 72 []caveatedUpdate{ 73 {core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:sarah", "testcaveat", nil}, 74 {core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:john", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, 75 {core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:jane", "", nil}, 76 {core.RelationTupleUpdate_CREATE, "document:foo#org@organization:someorg", "anothercaveat", nil}, 77 {core.RelationTupleUpdate_CREATE, "document:bar#org@organization:someorg", "", nil}, 78 {core.RelationTupleUpdate_CREATE, "document:foo#editor@user:vic", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, 79 {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:vic", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}}, 80 {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:blippy", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, 81 {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:noa", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, 82 {core.RelationTupleUpdate_CREATE, "document:foo#editor@user:noa", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}}, 83 {core.RelationTupleUpdate_CREATE, "document:foo#editor@user:wayne", "invalid", nil}, 84 }, 85 []check{ 86 { 87 "document:foo#view@user:sarah", 88 nil, 89 v1.ResourceCheckResult_CAVEATED_MEMBER, 90 []string{"anothercondition"}, 91 "", 92 }, 93 { 94 "document:foo#view@user:sarah", 95 map[string]any{ 96 "somecondition": "42", 97 }, 98 v1.ResourceCheckResult_CAVEATED_MEMBER, 99 []string{"anothercondition"}, 100 "", 101 }, 102 { 103 "document:foo#view@user:sarah", 104 map[string]any{ 105 "anothercondition": "15", 106 }, 107 v1.ResourceCheckResult_CAVEATED_MEMBER, 108 []string{"somecondition", "somebool"}, 109 "", 110 }, 111 { 112 "document:foo#view@user:sarah", 113 map[string]any{ 114 "somecondition": "42", 115 "anothercondition": "15", 116 "somebool": true, 117 }, 118 v1.ResourceCheckResult_MEMBER, 119 nil, 120 "", 121 }, 122 { 123 "document:foo#view@user:john", 124 map[string]any{ 125 "anothercondition": "14", 126 }, 127 v1.ResourceCheckResult_NOT_MEMBER, 128 nil, 129 "", 130 }, 131 { 132 "document:foo#view@user:john", 133 map[string]any{ 134 "anothercondition": "15", 135 }, 136 v1.ResourceCheckResult_MEMBER, 137 nil, 138 "", 139 }, 140 { 141 "document:bar#view@user:jane", nil, v1.ResourceCheckResult_MEMBER, 142 nil, 143 "", 144 }, 145 { 146 "document:foo#view@user:peter", 147 nil, 148 v1.ResourceCheckResult_NOT_MEMBER, 149 nil, 150 "", 151 }, 152 { 153 "document:foo#view@user:vic", 154 nil, 155 v1.ResourceCheckResult_MEMBER, 156 nil, 157 "", 158 }, 159 { 160 "document:foo#view@user:blippy", 161 nil, 162 v1.ResourceCheckResult_NOT_MEMBER, 163 nil, 164 "", 165 }, 166 { 167 "document:foo#view@user:noa", 168 nil, 169 v1.ResourceCheckResult_NOT_MEMBER, 170 nil, 171 "", 172 }, 173 { 174 "document:foo#view@user:wayne", 175 nil, 176 v1.ResourceCheckResult_MEMBER, 177 nil, 178 "caveat with name `invalid` not found", 179 }, 180 }, 181 }, 182 { 183 "overridden context test", 184 `definition user {} 185 186 definition document { 187 relation viewer: user | user with testcaveat 188 permission view = viewer 189 } 190 191 caveat testcaveat(somecondition int) { 192 somecondition == 42 193 } 194 `, 195 []caveatedUpdate{ 196 {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:tom", "testcaveat", map[string]any{ 197 "somecondition": 41, // not allowed 198 }}, 199 }, 200 []check{ 201 { 202 "document:foo#view@user:tom", 203 nil, 204 v1.ResourceCheckResult_NOT_MEMBER, 205 nil, 206 "", 207 }, 208 { 209 "document:foo#view@user:tom", 210 map[string]any{ 211 "somecondition": 42, // still not a member, because the written value takes precedence 212 }, 213 v1.ResourceCheckResult_NOT_MEMBER, 214 nil, 215 "", 216 }, 217 }, 218 }, 219 { 220 "intersection test", 221 `definition user {} 222 223 definition document { 224 relation viewer: user | user with viewcaveat 225 relation editor: user | user with editcaveat 226 permission view_and_edit = viewer & editor 227 } 228 229 caveat viewcaveat(somecondition int) { 230 somecondition == 42 231 } 232 233 caveat editcaveat(today string) { 234 today == 'tuesday' 235 } 236 `, 237 []caveatedUpdate{ 238 { 239 core.RelationTupleUpdate_CREATE, 240 "document:foo#viewer@user:tom", 241 "viewcaveat", 242 nil, 243 }, 244 { 245 core.RelationTupleUpdate_CREATE, 246 "document:foo#editor@user:tom", 247 "editcaveat", 248 nil, 249 }, 250 }, 251 []check{ 252 { 253 "document:foo#view_and_edit@user:tom", 254 map[string]any{ 255 "somecondition": "42", 256 "today": "wednesday", 257 }, 258 v1.ResourceCheckResult_NOT_MEMBER, 259 nil, 260 "", 261 }, 262 { 263 "document:foo#view_and_edit@user:tom", 264 map[string]any{ 265 "somecondition": "41", 266 "today": "tuesday", 267 }, 268 v1.ResourceCheckResult_NOT_MEMBER, 269 nil, 270 "", 271 }, 272 { 273 "document:foo#view_and_edit@user:tom", 274 map[string]any{ 275 "somecondition": "42", 276 "today": "tuesday", 277 }, 278 v1.ResourceCheckResult_MEMBER, 279 nil, 280 "", 281 }, 282 }, 283 }, 284 { 285 "exclusion test", 286 `definition user {} 287 288 definition document { 289 relation viewer: user | user with viewcaveat 290 relation banned: user | user with bannedcaveat 291 permission view_not_banned = viewer - banned 292 } 293 294 caveat viewcaveat(somecondition int) { 295 somecondition == 42 296 } 297 298 caveat bannedcaveat(region string) { 299 region == 'bad' 300 } 301 `, 302 []caveatedUpdate{ 303 { 304 core.RelationTupleUpdate_CREATE, 305 "document:foo#viewer@user:tom", 306 "viewcaveat", 307 nil, 308 }, 309 { 310 core.RelationTupleUpdate_CREATE, 311 "document:foo#banned@user:tom", 312 "bannedcaveat", 313 nil, 314 }, 315 }, 316 []check{ 317 { 318 "document:foo#view_not_banned@user:tom", 319 map[string]any{ 320 "somecondition": "42", 321 "region": "bad", 322 }, 323 v1.ResourceCheckResult_NOT_MEMBER, 324 nil, 325 "", 326 }, 327 { 328 "document:foo#view_not_banned@user:tom", 329 map[string]any{ 330 "somecondition": "41", 331 "region": "good", 332 }, 333 v1.ResourceCheckResult_NOT_MEMBER, 334 nil, 335 "", 336 }, 337 { 338 "document:foo#view_not_banned@user:tom", 339 map[string]any{ 340 "somecondition": "42", 341 "region": "good", 342 }, 343 v1.ResourceCheckResult_MEMBER, 344 nil, 345 "", 346 }, 347 }, 348 }, 349 { 350 "IP Allowlists example", 351 `definition user {} 352 353 definition organization { 354 relation members: user 355 relation ip_allowlist_policy: organization#members | organization#members with ip_allowlist 356 357 permission policy = ip_allowlist_policy 358 } 359 360 definition repository { 361 relation owner: organization 362 relation reader: user 363 364 permission read = reader & owner->policy 365 } 366 367 caveat ip_allowlist(user_ip ipaddress, cidr string) { 368 user_ip.in_cidr(cidr) 369 } 370 `, 371 []caveatedUpdate{ 372 { 373 core.RelationTupleUpdate_CREATE, 374 "repository:foobar#owner@organization:myorg", 375 "", 376 nil, 377 }, 378 { 379 core.RelationTupleUpdate_CREATE, 380 "organization:myorg#members@user:johndoe", 381 "", 382 nil, 383 }, 384 { 385 core.RelationTupleUpdate_CREATE, 386 "repository:foobar#reader@user:johndoe", 387 "", 388 nil, 389 }, 390 { 391 core.RelationTupleUpdate_CREATE, 392 "organization:myorg#ip_allowlist_policy@organization:myorg#members", 393 "ip_allowlist", 394 map[string]any{ 395 "cidr": "192.168.0.0/16", 396 }, 397 }, 398 }, 399 []check{ 400 { 401 "repository:foobar#read@user:johndoe", 402 nil, 403 v1.ResourceCheckResult_CAVEATED_MEMBER, 404 []string{"user_ip"}, 405 "", 406 }, 407 { 408 "repository:foobar#read@user:johndoe", 409 map[string]any{ 410 "user_ip": types.MustParseIPAddress("192.168.0.1"), 411 }, 412 v1.ResourceCheckResult_MEMBER, 413 nil, 414 "", 415 }, 416 { 417 "repository:foobar#read@user:johndoe", 418 map[string]any{ 419 "user_ip": types.MustParseIPAddress("9.2.3.1"), 420 }, 421 v1.ResourceCheckResult_NOT_MEMBER, 422 nil, 423 "", 424 }, 425 { 426 "repository:foobar#read@user:johndoe", 427 map[string]any{ 428 "user_ip": "192.168.0.1", 429 }, 430 v1.ResourceCheckResult_MEMBER, 431 nil, 432 "", 433 }, 434 }, 435 }, 436 { 437 "App attributes example", 438 `definition application {} 439 definition group { 440 relation member: application | application with attributes_match 441 permission allowed = member 442 } 443 444 caveat attributes_match(expected map<any>, provided map<any>) { 445 expected.isSubtreeOf(provided) 446 } 447 `, 448 []caveatedUpdate{ 449 { 450 core.RelationTupleUpdate_CREATE, 451 "group:ui_apps#member@application:frontend_app", 452 "attributes_match", 453 map[string]any{ 454 "expected": map[string]any{"type": "frontend", "region": "eu"}, 455 }, 456 }, 457 { 458 core.RelationTupleUpdate_CREATE, 459 "group:backend_apps#member@application:backend_app", 460 "attributes_match", 461 map[string]any{ 462 "expected": map[string]any{ 463 "type": "backend", "region": "us", 464 "additional_attrs": map[string]any{ 465 "tag1": 100, 466 "tag2": false, 467 }, 468 }, 469 }, 470 }, 471 }, 472 []check{ 473 { 474 "group:ui_apps#allowed@application:frontend_app", 475 map[string]any{ 476 "provided": map[string]any{"type": "frontend", "region": "eu", "team": "shop"}, 477 }, 478 v1.ResourceCheckResult_MEMBER, 479 nil, 480 "", 481 }, 482 { 483 "group:ui_apps#allowed@application:frontend_app", 484 map[string]any{ 485 "provided": map[string]any{"type": "frontend", "region": "us"}, 486 }, 487 v1.ResourceCheckResult_NOT_MEMBER, 488 nil, 489 "", 490 }, 491 { 492 "group:backend_apps#allowed@application:backend_app", 493 map[string]any{ 494 "provided": map[string]any{ 495 "type": "backend", "region": "us", "team": "shop", 496 "additional_attrs": map[string]any{ 497 "tag1": 100.0, 498 "tag2": false, 499 "tag3": "hi", 500 }, 501 }, 502 }, 503 v1.ResourceCheckResult_MEMBER, 504 nil, 505 "", 506 }, 507 { 508 "group:backend_apps#allowed@application:backend_app", 509 map[string]any{ 510 "provided": map[string]any{ 511 "type": "backend", "region": "us", "team": "shop", 512 "additional_attrs": map[string]any{ 513 "tag1": 200.0, 514 "tag2": false, 515 }, 516 }, 517 }, 518 v1.ResourceCheckResult_NOT_MEMBER, 519 nil, 520 "", 521 }, 522 }, 523 }, 524 { 525 "authorize if resource was created before subject", 526 `definition root { 527 relation actors: actor 528 } 529 definition resource { 530 relation creation_policy: root#actors | root#actors with created_before 531 permission tag = creation_policy 532 } 533 534 definition actor {} 535 536 caveat created_before(actor_created_at string, created_at string) { 537 timestamp(actor_created_at) > timestamp(created_at) 538 } 539 `, 540 []caveatedUpdate{ 541 { 542 core.RelationTupleUpdate_CREATE, 543 "resource:foo#creation_policy@root:root#actors", 544 "created_before", 545 map[string]any{ 546 "created_at": "2022-01-01T10:00:00.021Z", 547 }, 548 }, 549 { 550 core.RelationTupleUpdate_CREATE, 551 "root:root#actors@actor:johndoe", 552 "", 553 nil, 554 }, 555 }, 556 []check{ 557 { 558 "resource:foo#tag@actor:johndoe", 559 map[string]any{ 560 "actor_created_at": "2022-01-01T11:00:00.021Z", 561 }, 562 v1.ResourceCheckResult_MEMBER, 563 nil, 564 "", 565 }, 566 { 567 "resource:foo#tag@actor:johndoe", 568 map[string]any{ 569 "actor_created_at": "2022-01-01T09:00:00.021Z", 570 }, 571 v1.ResourceCheckResult_NOT_MEMBER, 572 nil, 573 "", 574 }, 575 }, 576 }, 577 { 578 "time-bound permission", 579 `definition resource { 580 relation reader: user | user with not_expired 581 permission view = reader 582 } 583 584 caveat not_expired(expiration string, now string) { 585 timestamp(now) < timestamp(expiration) 586 } 587 588 definition user {}`, 589 []caveatedUpdate{ 590 { 591 core.RelationTupleUpdate_CREATE, 592 "resource:foo#reader@user:sarah", 593 "not_expired", 594 map[string]any{ 595 "expiration": "2030-01-01T10:00:00.021Z", 596 "now": "2020-01-01T10:00:00.021Z", 597 }, 598 }, 599 { 600 core.RelationTupleUpdate_CREATE, 601 "resource:foo#reader@user:john", 602 "not_expired", 603 map[string]any{ 604 "expiration": "2020-01-01T10:00:00.021Z", 605 "now": "2020-01-01T10:00:00.021Z", 606 }, 607 }, 608 }, 609 []check{ 610 { 611 "resource:foo#view@user:sarah", 612 nil, 613 v1.ResourceCheckResult_MEMBER, 614 nil, 615 "", 616 }, 617 { 618 "resource:foo#view@user:john", 619 nil, 620 v1.ResourceCheckResult_NOT_MEMBER, 621 nil, 622 "", 623 }, 624 }, 625 }, 626 { 627 "legal-guardian example", 628 `definition claim { 629 relation claimer: user 630 relation dependent_of: user#dependent_of | user#dependent_of with legal_guardian 631 632 permission view = claimer + dependent_of 633 } 634 635 caveat legal_guardian(age int, class string) { 636 age < 12 || (class != "sensitive" && age > 12 && age < 18) 637 } 638 639 definition user { 640 relation dependent_of: user 641 }`, 642 []caveatedUpdate{ 643 { 644 core.RelationTupleUpdate_CREATE, 645 "user:son#dependent_of@user:father", 646 "", 647 nil, 648 }, 649 { 650 core.RelationTupleUpdate_CREATE, 651 "claim:broken_leg#dependent_of@user:son#dependent_of", 652 "legal_guardian", 653 map[string]any{ 654 "age": 10, 655 "class": "non-sensitive", 656 }, 657 }, 658 { 659 core.RelationTupleUpdate_CREATE, 660 "user:daughter#dependent_of@user:father", 661 "", 662 nil, 663 }, 664 { 665 core.RelationTupleUpdate_CREATE, 666 "claim:broken_arm#dependent_of@user:daughter#dependent_of", 667 "legal_guardian", 668 map[string]any{ 669 "age": 14, 670 "class": "non-sensitive", 671 }, 672 }, 673 { 674 core.RelationTupleUpdate_CREATE, 675 "claim:sensitive_matter#dependent_of@user:daughter#dependent_of", 676 "legal_guardian", 677 map[string]any{ 678 "age": 14, 679 "class": "sensitive", 680 }, 681 }, 682 }, 683 []check{ 684 { 685 "claim:broken_leg#view@user:father", 686 nil, 687 v1.ResourceCheckResult_MEMBER, 688 nil, 689 "", 690 }, 691 { 692 "claim:broken_arm#view@user:father", 693 nil, 694 v1.ResourceCheckResult_MEMBER, 695 nil, 696 "", 697 }, 698 { 699 "claim:sensitive_matter#view@user:father", 700 nil, 701 v1.ResourceCheckResult_NOT_MEMBER, 702 nil, 703 "", 704 }, 705 }, 706 }, 707 { 708 "context type error test", 709 `definition user {} 710 711 definition document { 712 relation viewer: user | user with testcaveat 713 714 permission view = viewer 715 } 716 717 caveat testcaveat(somecondition uint) { 718 somecondition == 42 719 } 720 `, 721 []caveatedUpdate{ 722 {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:sarah", "testcaveat", nil}, 723 }, 724 []check{ 725 { 726 "document:foo#view@user:sarah", 727 map[string]any{ 728 "somecondition": "43a", 729 }, 730 v1.ResourceCheckResult_NOT_MEMBER, 731 []string{}, 732 "type error for parameters for caveat `testcaveat`: could not convert context parameter `somecondition`: for uint: a uint64 value is required, but found invalid string value `43a`", 733 }, 734 { 735 "document:foo#view@user:sarah", 736 map[string]any{ 737 "somecondition": "-43", 738 }, 739 v1.ResourceCheckResult_NOT_MEMBER, 740 []string{}, 741 "type error for parameters for caveat `testcaveat`: could not convert context parameter `somecondition`: for uint: a uint value is required, but found int64 value `-43`", 742 }, 743 }, 744 }, 745 { 746 "schema caveat test", 747 ` 748 caveat testcaveat(somecondition uint) { 749 somecondition == 42 750 } 751 752 definition user {} 753 754 definition document { 755 relation viewer: user with testcaveat 756 757 permission view = viewer 758 }`, 759 []caveatedUpdate{ 760 {core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:sarah", "testcaveat", nil}, 761 }, 762 []check{ 763 { 764 "document:foo#view@user:sarah", 765 map[string]any{ 766 "somecondition": "43a", 767 }, 768 v1.ResourceCheckResult_NOT_MEMBER, 769 []string{}, 770 "type error for parameters for caveat `testcaveat`: could not convert context parameter `somecondition`: for uint: a uint64 value is required, but found invalid string value `43a`", 771 }, 772 { 773 "document:foo#view@user:sarah", 774 map[string]any{ 775 "somecondition": "-43", 776 }, 777 v1.ResourceCheckResult_NOT_MEMBER, 778 []string{}, 779 "type error for parameters for caveat `testcaveat`: could not convert context parameter `somecondition`: for uint: a uint value is required, but found int64 value `-43`", 780 }, 781 { 782 "document:foo#view@user:sarah", 783 map[string]any{ 784 "somecondition": "41", 785 }, 786 v1.ResourceCheckResult_NOT_MEMBER, 787 []string{}, 788 "", 789 }, 790 { 791 "document:foo#view@user:sarah", 792 map[string]any{ 793 "somecondition": "42", 794 }, 795 v1.ResourceCheckResult_MEMBER, 796 []string{}, 797 "", 798 }, 799 }, 800 }, 801 } 802 803 for _, tt := range testCases { 804 tt := tt 805 t.Run(tt.name, func(t *testing.T) { 806 ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 807 require.NoError(t, err) 808 809 dispatch := graph.NewLocalOnlyDispatcher(10) 810 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 811 require.NoError(t, datastoremw.SetInContext(ctx, ds)) 812 813 revision, err := writeCaveatedTuples(ctx, t, ds, tt.schema, tt.updates) 814 require.NoError(t, err) 815 816 for _, r := range tt.checks { 817 r := r 818 t.Run(fmt.Sprintf("%s::%v", r.check, r.context), func(t *testing.T) { 819 rel := tuple.MustParse(r.check) 820 821 result, _, err := computed.ComputeCheck(ctx, dispatch, 822 computed.CheckParameters{ 823 ResourceType: &core.RelationReference{ 824 Namespace: rel.ResourceAndRelation.Namespace, 825 Relation: rel.ResourceAndRelation.Relation, 826 }, 827 Subject: rel.Subject, 828 CaveatContext: r.context, 829 AtRevision: revision, 830 MaximumDepth: 50, 831 DebugOption: computed.BasicDebuggingEnabled, 832 }, 833 rel.ResourceAndRelation.ObjectId, 834 ) 835 836 if r.error != "" { 837 require.NotNil(t, err, "missing required error: %s", r.error) 838 require.Equal(t, err.Error(), r.error) 839 } else { 840 require.NoError(t, err) 841 require.Equal(t, v1.ResourceCheckResult_Membership_name[int32(r.member)], v1.ResourceCheckResult_Membership_name[int32(result.Membership)], "mismatch for %s with context %v", r.check, r.context) 842 843 if result.Membership == v1.ResourceCheckResult_CAVEATED_MEMBER { 844 require.Equal(t, r.expectedMissingFields, result.MissingExprFields) 845 } 846 } 847 }) 848 } 849 }) 850 } 851 } 852 853 func TestComputeCheckError(t *testing.T) { 854 ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 855 require.NoError(t, err) 856 857 dispatch := graph.NewLocalOnlyDispatcher(10) 858 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 859 require.NoError(t, datastoremw.SetInContext(ctx, ds)) 860 861 _, _, err = computed.ComputeCheck(ctx, dispatch, 862 computed.CheckParameters{ 863 ResourceType: &core.RelationReference{ 864 Namespace: "a", 865 Relation: "b", 866 }, 867 Subject: &core.ObjectAndRelation{}, 868 CaveatContext: nil, 869 AtRevision: datastore.NoRevision, 870 MaximumDepth: 50, 871 DebugOption: computed.BasicDebuggingEnabled, 872 }, 873 "id", 874 ) 875 require.Error(t, err) 876 } 877 878 func TestComputeBulkCheck(t *testing.T) { 879 ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) 880 require.NoError(t, err) 881 882 dispatch := graph.NewLocalOnlyDispatcher(10) 883 ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background())) 884 require.NoError(t, datastoremw.SetInContext(ctx, ds)) 885 886 revision, err := writeCaveatedTuples(ctx, t, ds, ` 887 definition user {} 888 889 caveat somecaveat(somecondition int) { 890 somecondition == 42 891 } 892 893 definition document { 894 relation viewer: user | user with somecaveat 895 permission view = viewer 896 } 897 `, []caveatedUpdate{ 898 {core.RelationTupleUpdate_CREATE, "document:direct#viewer@user:tom", "", nil}, 899 {core.RelationTupleUpdate_CREATE, "document:first#viewer@user:tom", "somecaveat", map[string]any{ 900 "somecondition": 42, 901 }}, 902 {core.RelationTupleUpdate_CREATE, "document:second#viewer@user:tom", "somecaveat", map[string]any{}}, 903 {core.RelationTupleUpdate_CREATE, "document:third#viewer@user:tom", "somecaveat", map[string]any{ 904 "somecondition": 32, 905 }}, 906 }) 907 require.NoError(t, err) 908 909 resp, _, err := computed.ComputeBulkCheck(ctx, dispatch, 910 computed.CheckParameters{ 911 ResourceType: &core.RelationReference{ 912 Namespace: "document", 913 Relation: "view", 914 }, 915 Subject: &core.ObjectAndRelation{ 916 Namespace: "user", 917 ObjectId: "tom", 918 Relation: "...", 919 }, 920 CaveatContext: nil, 921 AtRevision: revision, 922 MaximumDepth: 50, 923 DebugOption: computed.NoDebugging, 924 }, 925 []string{"direct", "first", "second", "third"}, 926 ) 927 require.NoError(t, err) 928 929 require.Equal(t, resp["direct"].Membership, v1.ResourceCheckResult_MEMBER) 930 require.Equal(t, resp["first"].Membership, v1.ResourceCheckResult_MEMBER) 931 require.Equal(t, resp["second"].Membership, v1.ResourceCheckResult_CAVEATED_MEMBER) 932 require.Equal(t, resp["third"].Membership, v1.ResourceCheckResult_NOT_MEMBER) 933 } 934 935 func writeCaveatedTuples(ctx context.Context, _ *testing.T, ds datastore.Datastore, schema string, updates []caveatedUpdate) (datastore.Revision, error) { 936 compiled, err := compiler.Compile(compiler.InputSchema{ 937 Source: "schema", 938 SchemaString: schema, 939 }, compiler.AllowUnprefixedObjectType()) 940 if err != nil { 941 return datastore.NoRevision, err 942 } 943 944 return ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { 945 if err := rwt.WriteNamespaces(ctx, compiled.ObjectDefinitions...); err != nil { 946 return err 947 } 948 949 if err := rwt.WriteCaveats(ctx, compiled.CaveatDefinitions); err != nil { 950 return err 951 } 952 953 var rtu []*core.RelationTupleUpdate 954 for _, updt := range updates { 955 rtu = append(rtu, &core.RelationTupleUpdate{ 956 Operation: updt.Operation, 957 Tuple: caveatedRelationTuple(updt.tuple, updt.caveatName, updt.context), 958 }) 959 } 960 961 return rwt.WriteRelationships(ctx, rtu) 962 }) 963 } 964 965 func caveatedRelationTuple(relationTuple string, caveatName string, context map[string]any) *core.RelationTuple { 966 c := tuple.MustParse(relationTuple) 967 strct, err := structpb.NewStruct(context) 968 if err != nil { 969 panic(err) 970 } 971 if caveatName != "" { 972 c.Caveat = &core.ContextualizedCaveat{ 973 CaveatName: caveatName, 974 Context: strct, 975 } 976 } 977 return c 978 }