github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/permissions_test.go (about) 1 package v1_test 2 3 import ( 4 "cmp" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "math/rand" 10 "slices" 11 "sort" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/authzed/authzed-go/pkg/requestmeta" 17 "github.com/authzed/authzed-go/pkg/responsemeta" 18 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 19 "github.com/authzed/grpcutil" 20 "github.com/stretchr/testify/require" 21 "go.uber.org/goleak" 22 "google.golang.org/grpc" 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/metadata" 25 "google.golang.org/grpc/status" 26 "google.golang.org/protobuf/types/known/structpb" 27 28 "github.com/authzed/spicedb/internal/datastore/memdb" 29 "github.com/authzed/spicedb/internal/namespace" 30 "github.com/authzed/spicedb/internal/services/shared" 31 v1svc "github.com/authzed/spicedb/internal/services/v1" 32 tf "github.com/authzed/spicedb/internal/testfixtures" 33 "github.com/authzed/spicedb/internal/testserver" 34 itestutil "github.com/authzed/spicedb/internal/testutil" 35 "github.com/authzed/spicedb/pkg/datastore" 36 "github.com/authzed/spicedb/pkg/genutil/mapz" 37 pgraph "github.com/authzed/spicedb/pkg/graph" 38 core "github.com/authzed/spicedb/pkg/proto/core/v1" 39 "github.com/authzed/spicedb/pkg/schemadsl/compiler" 40 "github.com/authzed/spicedb/pkg/schemadsl/input" 41 "github.com/authzed/spicedb/pkg/testutil" 42 "github.com/authzed/spicedb/pkg/tuple" 43 "github.com/authzed/spicedb/pkg/zedtoken" 44 ) 45 46 var testTimedeltas = []time.Duration{0, 1 * time.Second} 47 48 func obj(objType, objID string) *v1.ObjectReference { 49 return &v1.ObjectReference{ 50 ObjectType: objType, 51 ObjectId: objID, 52 } 53 } 54 55 func sub(subType string, subID string, subRel string) *v1.SubjectReference { 56 return &v1.SubjectReference{ 57 Object: obj(subType, subID), 58 OptionalRelation: subRel, 59 } 60 } 61 62 func TestCheckPermissions(t *testing.T) { 63 testCases := []struct { 64 resource *v1.ObjectReference 65 permission string 66 subject *v1.SubjectReference 67 expected v1.CheckPermissionResponse_Permissionship 68 expectedStatus codes.Code 69 }{ 70 { 71 obj("document", "masterplan"), 72 "view", 73 sub("user", "eng_lead", ""), 74 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 75 codes.OK, 76 }, 77 { 78 obj("document", "masterplan"), 79 "view", 80 sub("user", "product_manager", ""), 81 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 82 codes.OK, 83 }, 84 { 85 obj("document", "masterplan"), 86 "view", 87 sub("user", "chief_financial_officer", ""), 88 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 89 codes.OK, 90 }, 91 { 92 obj("document", "healthplan"), 93 "view", 94 sub("user", "chief_financial_officer", ""), 95 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 96 codes.OK, 97 }, 98 { 99 obj("document", "masterplan"), 100 "view", 101 sub("user", "auditor", ""), 102 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 103 codes.OK, 104 }, 105 { 106 obj("document", "companyplan"), 107 "view", 108 sub("user", "auditor", ""), 109 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 110 codes.OK, 111 }, 112 { 113 obj("document", "masterplan"), 114 "view", 115 sub("user", "vp_product", ""), 116 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 117 codes.OK, 118 }, 119 { 120 obj("document", "masterplan"), 121 "view", 122 sub("user", "legal", ""), 123 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 124 codes.OK, 125 }, 126 { 127 obj("document", "companyplan"), 128 "view", 129 sub("user", "legal", ""), 130 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 131 codes.OK, 132 }, 133 { 134 obj("document", "masterplan"), 135 "view", 136 sub("user", "owner", ""), 137 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 138 codes.OK, 139 }, 140 { 141 obj("document", "companyplan"), 142 "view", 143 sub("user", "owner", ""), 144 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 145 codes.OK, 146 }, 147 { 148 obj("document", "masterplan"), 149 "view", 150 sub("user", "villain", ""), 151 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 152 codes.OK, 153 }, 154 { 155 obj("document", "masterplan"), 156 "view", 157 sub("user", "unknowngal", ""), 158 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 159 codes.OK, 160 }, 161 { 162 obj("document", "masterplan"), 163 "view_and_edit", 164 sub("user", "eng_lead", ""), 165 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 166 codes.OK, 167 }, 168 { 169 obj("document", "specialplan"), 170 "view_and_edit", 171 sub("user", "multiroleguy", ""), 172 v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 173 codes.OK, 174 }, 175 { 176 obj("document", "masterplan"), 177 "view_and_edit", 178 sub("user", "missingrolegal", ""), 179 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 180 codes.OK, 181 }, 182 { 183 obj("document", "masterplan"), 184 "invalidrelation", 185 sub("user", "missingrolegal", ""), 186 v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, 187 codes.FailedPrecondition, 188 }, 189 { 190 obj("document", "masterplan"), 191 "view_and_edit", 192 sub("user", "someuser", "invalidrelation"), 193 v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, 194 codes.FailedPrecondition, 195 }, 196 { 197 obj("invalidnamespace", "masterplan"), 198 "view_and_edit", 199 sub("user", "someuser", ""), 200 v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, 201 codes.FailedPrecondition, 202 }, 203 { 204 obj("document", "masterplan"), 205 "view_and_edit", 206 sub("invalidnamespace", "someuser", ""), 207 v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, 208 codes.FailedPrecondition, 209 }, 210 { 211 obj("document", "*"), 212 "view_and_edit", 213 sub("invalidnamespace", "someuser", ""), 214 v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, 215 codes.InvalidArgument, 216 }, 217 { 218 obj("document", "something"), 219 "view", 220 sub("user", "*", ""), 221 v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, 222 codes.InvalidArgument, 223 }, 224 { 225 obj("document", "something"), 226 "unknown", 227 sub("user", "foo", ""), 228 v1.CheckPermissionResponse_PERMISSIONSHIP_UNSPECIFIED, 229 codes.FailedPrecondition, 230 }, 231 { 232 obj("document", "-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK=="), 233 "view", 234 sub("user", "unkn-base64YWZzZGZh-ZHNmZHPwn5iK8J+YivC/fmIrwn5iK==owngal", ""), 235 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 236 codes.OK, 237 }, 238 { 239 obj("document", "foo"), 240 "*", 241 sub("user", "bar", ""), 242 v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 243 codes.InvalidArgument, 244 }, 245 } 246 247 for _, delta := range testTimedeltas { 248 delta := delta 249 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 250 for _, debug := range []bool{false, true} { 251 debug := debug 252 t.Run(fmt.Sprintf("debug%v", debug), func(t *testing.T) { 253 for _, tc := range testCases { 254 tc := tc 255 t.Run(fmt.Sprintf( 256 "%s:%s#%s@%s:%s#%s", 257 tc.resource.ObjectType, 258 tc.resource.ObjectId, 259 tc.permission, 260 tc.subject.Object.ObjectType, 261 tc.subject.Object.ObjectId, 262 tc.subject.OptionalRelation, 263 ), func(t *testing.T) { 264 require := require.New(t) 265 conn, cleanup, _, revision := testserver.NewTestServer(require, delta, memdb.DisableGC, true, tf.StandardDatastoreWithData) 266 client := v1.NewPermissionsServiceClient(conn) 267 t.Cleanup(cleanup) 268 269 ctx := context.Background() 270 if debug { 271 ctx = requestmeta.AddRequestHeaders(ctx, requestmeta.RequestDebugInformation) 272 } 273 274 var trailer metadata.MD 275 checkResp, err := client.CheckPermission(ctx, &v1.CheckPermissionRequest{ 276 Consistency: &v1.Consistency{ 277 Requirement: &v1.Consistency_AtLeastAsFresh{ 278 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 279 }, 280 }, 281 Resource: tc.resource, 282 Permission: tc.permission, 283 Subject: tc.subject, 284 }, grpc.Trailer(&trailer)) 285 286 if tc.expectedStatus == codes.OK { 287 require.NoError(err) 288 require.Equal(tc.expected, checkResp.Permissionship) 289 290 dispatchCount, err := responsemeta.GetIntResponseTrailerMetadata(trailer, responsemeta.DispatchedOperationsCount) 291 require.NoError(err) 292 require.GreaterOrEqual(dispatchCount, 0) 293 294 encodedDebugInfo, err := responsemeta.GetResponseTrailerMetadataOrNil(trailer, responsemeta.DebugInformation) 295 require.NoError(err) 296 297 if debug { 298 require.Nil(encodedDebugInfo) 299 300 debugInfo := checkResp.DebugTrace 301 require.NotNil(debugInfo.Check) 302 require.NotNil(debugInfo.Check.Duration) 303 require.Equal(tuple.StringObjectRef(tc.resource), tuple.StringObjectRef(debugInfo.Check.Resource)) 304 require.Equal(tc.permission, debugInfo.Check.Permission) 305 require.Equal(tuple.StringSubjectRef(tc.subject), tuple.StringSubjectRef(debugInfo.Check.Subject)) 306 } else { 307 require.Nil(encodedDebugInfo) 308 } 309 } else { 310 grpcutil.RequireStatus(t, tc.expectedStatus, err) 311 } 312 }) 313 } 314 }) 315 } 316 }) 317 } 318 } 319 320 func TestCheckPermissionWithDebugInfo(t *testing.T) { 321 require := require.New(t) 322 conn, cleanup, _, revision := testserver.NewTestServer(require, testTimedeltas[0], memdb.DisableGC, true, tf.StandardDatastoreWithData) 323 client := v1.NewPermissionsServiceClient(conn) 324 t.Cleanup(cleanup) 325 326 ctx := context.Background() 327 ctx = requestmeta.AddRequestHeaders(ctx, requestmeta.RequestDebugInformation) 328 329 var trailer metadata.MD 330 checkResp, err := client.CheckPermission(ctx, &v1.CheckPermissionRequest{ 331 Consistency: &v1.Consistency{ 332 Requirement: &v1.Consistency_AtLeastAsFresh{ 333 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 334 }, 335 }, 336 Resource: obj("document", "masterplan"), 337 Permission: "view", 338 Subject: sub("user", "auditor", ""), 339 }, grpc.Trailer(&trailer)) 340 341 require.NoError(err) 342 require.Equal(v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, checkResp.Permissionship) 343 344 encodedDebugInfo, err := responsemeta.GetResponseTrailerMetadataOrNil(trailer, responsemeta.DebugInformation) 345 require.NoError(err) 346 347 // debug info is returned empty to make sure clients are not broken with backward incompatible payloads 348 require.Nil(encodedDebugInfo) 349 350 debugInfo := checkResp.DebugTrace 351 require.GreaterOrEqual(len(debugInfo.Check.GetSubProblems().Traces), 1) 352 require.NotEmpty(debugInfo.SchemaUsed) 353 354 // Compile the schema into the namespace definitions. 355 compiled, err := compiler.Compile(compiler.InputSchema{ 356 Source: input.Source("schema"), 357 SchemaString: debugInfo.SchemaUsed, 358 }, compiler.AllowUnprefixedObjectType()) 359 require.NoError(err, "Invalid schema: %s", debugInfo.SchemaUsed) 360 require.Equal(4, len(compiled.OrderedDefinitions)) 361 } 362 363 func TestLookupResources(t *testing.T) { 364 testCases := []struct { 365 objectType string 366 permission string 367 subject *v1.SubjectReference 368 expectedObjectIds []string 369 expectedErrorCode codes.Code 370 minimumDispatchCount int 371 maximumDispatchCount int 372 }{ 373 { 374 "document", "viewer", 375 sub("user", "eng_lead", ""), 376 []string{"masterplan"}, 377 codes.OK, 378 1, 379 1, 380 }, 381 { 382 "document", "view", 383 sub("user", "eng_lead", ""), 384 []string{"masterplan"}, 385 codes.OK, 386 2, 387 2, 388 }, 389 { 390 "document", "view", 391 sub("user", "product_manager", ""), 392 []string{"masterplan"}, 393 codes.OK, 394 3, 395 3, 396 }, 397 { 398 "document", "view", 399 sub("user", "chief_financial_officer", ""), 400 []string{"masterplan", "healthplan"}, 401 codes.OK, 402 3, 403 3, 404 }, 405 { 406 "document", "view", 407 sub("user", "auditor", ""), 408 []string{"masterplan", "companyplan"}, 409 codes.OK, 410 5, 411 5, 412 }, 413 { 414 "document", "view", 415 sub("user", "vp_product", ""), 416 []string{"masterplan"}, 417 codes.OK, 418 4, 419 4, 420 }, 421 { 422 "document", "view", 423 sub("user", "legal", ""), 424 []string{"masterplan", "companyplan"}, 425 codes.OK, 426 4, 427 4, 428 }, 429 { 430 "document", "view", 431 sub("user", "owner", ""), 432 []string{"masterplan", "companyplan", "ownerplan"}, 433 codes.OK, 434 6, 435 6, 436 }, 437 { 438 "document", "view", 439 sub("user", "villain", ""), 440 nil, 441 codes.OK, 442 1, 443 1, 444 }, 445 { 446 "document", "view", 447 sub("user", "unknowngal", ""), 448 nil, 449 codes.OK, 450 1, 451 1, 452 }, 453 { 454 "document", "view_and_edit", 455 sub("user", "eng_lead", ""), 456 nil, 457 codes.OK, 458 1, 459 1, 460 }, 461 { 462 "document", "view_and_edit", 463 sub("user", "multiroleguy", ""), 464 []string{"specialplan"}, 465 codes.OK, 466 6, 467 7, 468 }, 469 { 470 "document", "view_and_edit", 471 sub("user", "missingrolegal", ""), 472 nil, 473 codes.OK, 474 1, 475 1, 476 }, 477 { 478 "document", "invalidrelation", 479 sub("user", "missingrolegal", ""), 480 []string{}, 481 codes.FailedPrecondition, 482 1, 483 1, 484 }, 485 { 486 "document", "view_and_edit", 487 sub("user", "someuser", "invalidrelation"), 488 []string{}, 489 codes.FailedPrecondition, 490 0, 491 0, 492 }, 493 { 494 "invalidnamespace", "view_and_edit", 495 sub("user", "someuser", ""), 496 []string{}, 497 codes.FailedPrecondition, 498 0, 499 0, 500 }, 501 { 502 "document", "view_and_edit", 503 sub("invalidnamespace", "someuser", ""), 504 []string{}, 505 codes.FailedPrecondition, 506 0, 507 0, 508 }, 509 { 510 "document", "view_and_edit", 511 sub("user", "*", ""), 512 []string{}, 513 codes.InvalidArgument, 514 0, 515 0, 516 }, 517 { 518 "document", "*", 519 sub("user", "someuser", ""), 520 []string{}, 521 codes.InvalidArgument, 522 0, 523 0, 524 }, 525 } 526 527 for _, delta := range testTimedeltas { 528 delta := delta 529 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 530 for _, tc := range testCases { 531 tc := tc 532 t.Run(fmt.Sprintf("%s::%s from %s:%s#%s", tc.objectType, tc.permission, tc.subject.Object.ObjectType, tc.subject.Object.ObjectId, tc.subject.OptionalRelation), func(t *testing.T) { 533 require := require.New(t) 534 conn, cleanup, _, revision := testserver.NewTestServer(require, delta, memdb.DisableGC, true, tf.StandardDatastoreWithData) 535 client := v1.NewPermissionsServiceClient(conn) 536 t.Cleanup(func() { 537 goleak.VerifyNone(t, goleak.IgnoreCurrent()) 538 }) 539 t.Cleanup(cleanup) 540 541 var trailer metadata.MD 542 lookupClient, err := client.LookupResources(context.Background(), &v1.LookupResourcesRequest{ 543 ResourceObjectType: tc.objectType, 544 Permission: tc.permission, 545 Subject: tc.subject, 546 Consistency: &v1.Consistency{ 547 Requirement: &v1.Consistency_AtLeastAsFresh{ 548 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 549 }, 550 }, 551 }, grpc.Trailer(&trailer)) 552 553 require.NoError(err) 554 if tc.expectedErrorCode == codes.OK { 555 var resolvedObjectIds []string 556 for { 557 resp, err := lookupClient.Recv() 558 if errors.Is(err, io.EOF) { 559 break 560 } 561 562 require.NoError(err) 563 564 resolvedObjectIds = append(resolvedObjectIds, resp.ResourceObjectId) 565 } 566 567 slices.Sort(tc.expectedObjectIds) 568 slices.Sort(resolvedObjectIds) 569 570 require.Equal(tc.expectedObjectIds, resolvedObjectIds) 571 572 dispatchCount, err := responsemeta.GetIntResponseTrailerMetadata(trailer, responsemeta.DispatchedOperationsCount) 573 require.NoError(err) 574 require.GreaterOrEqual(dispatchCount, 0) 575 require.LessOrEqual(dispatchCount, tc.maximumDispatchCount) 576 require.GreaterOrEqual(dispatchCount, tc.minimumDispatchCount) 577 } else { 578 _, err := lookupClient.Recv() 579 grpcutil.RequireStatus(t, tc.expectedErrorCode, err) 580 } 581 }) 582 } 583 }) 584 } 585 } 586 587 func TestExpand(t *testing.T) { 588 testCases := []struct { 589 startObjectType string 590 startObjectID string 591 startPermission string 592 expandRelatedCount int 593 expectedErrorCode codes.Code 594 }{ 595 {"document", "masterplan", "owner", 1, codes.OK}, 596 {"document", "masterplan", "view", 7, codes.OK}, 597 {"document", "masterplan", "fakerelation", 0, codes.FailedPrecondition}, 598 {"fake", "masterplan", "owner", 0, codes.FailedPrecondition}, 599 {"document", "", "owner", 1, codes.InvalidArgument}, 600 {"document", "somedoc", "*", 1, codes.InvalidArgument}, 601 } 602 603 for _, delta := range testTimedeltas { 604 delta := delta 605 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 606 for _, tc := range testCases { 607 tc := tc 608 t.Run(fmt.Sprintf("%s:%s#%s", tc.startObjectType, tc.startObjectID, tc.startPermission), func(t *testing.T) { 609 require := require.New(t) 610 conn, cleanup, _, revision := testserver.NewTestServer(require, delta, memdb.DisableGC, true, tf.StandardDatastoreWithData) 611 client := v1.NewPermissionsServiceClient(conn) 612 t.Cleanup(cleanup) 613 614 var trailer metadata.MD 615 expanded, err := client.ExpandPermissionTree(context.Background(), &v1.ExpandPermissionTreeRequest{ 616 Resource: &v1.ObjectReference{ 617 ObjectType: tc.startObjectType, 618 ObjectId: tc.startObjectID, 619 }, 620 Permission: tc.startPermission, 621 Consistency: &v1.Consistency{ 622 Requirement: &v1.Consistency_AtLeastAsFresh{ 623 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 624 }, 625 }, 626 }, grpc.Trailer(&trailer)) 627 if tc.expectedErrorCode == codes.OK { 628 require.NoError(err) 629 require.Equal(tc.expandRelatedCount, countLeafs(expanded.TreeRoot)) 630 631 dispatchCount, err := responsemeta.GetIntResponseTrailerMetadata(trailer, responsemeta.DispatchedOperationsCount) 632 require.NoError(err) 633 require.GreaterOrEqual(dispatchCount, 0) 634 } else { 635 grpcutil.RequireStatus(t, tc.expectedErrorCode, err) 636 } 637 }) 638 } 639 }) 640 } 641 } 642 643 func countLeafs(node *v1.PermissionRelationshipTree) int { 644 switch t := node.TreeType.(type) { 645 case *v1.PermissionRelationshipTree_Leaf: 646 return len(t.Leaf.Subjects) 647 648 case *v1.PermissionRelationshipTree_Intermediate: 649 count := 0 650 for _, child := range t.Intermediate.Children { 651 count += countLeafs(child) 652 } 653 return count 654 655 default: 656 panic("Unknown node type") 657 } 658 } 659 660 var ONR = tuple.ObjectAndRelation 661 662 func DS(objectType string, objectID string, objectRelation string) *core.DirectSubject { 663 return &core.DirectSubject{ 664 Subject: ONR(objectType, objectID, objectRelation), 665 } 666 } 667 668 func TestTranslateExpansionTree(t *testing.T) { 669 table := []struct { 670 name string 671 input *core.RelationTupleTreeNode 672 }{ 673 {"simple leaf", pgraph.Leaf(nil, (DS("user", "user1", "...")))}, 674 { 675 "simple union", 676 pgraph.Union(nil, 677 pgraph.Leaf(nil, (DS("user", "user1", "..."))), 678 pgraph.Leaf(nil, (DS("user", "user2", "..."))), 679 pgraph.Leaf(nil, (DS("user", "user3", "..."))), 680 ), 681 }, 682 { 683 "simple intersection", 684 pgraph.Intersection(nil, 685 pgraph.Leaf(nil, 686 (DS("user", "user1", "...")), 687 (DS("user", "user2", "...")), 688 ), 689 pgraph.Leaf(nil, 690 (DS("user", "user2", "...")), 691 (DS("user", "user3", "...")), 692 ), 693 pgraph.Leaf(nil, 694 (DS("user", "user2", "...")), 695 (DS("user", "user4", "...")), 696 ), 697 ), 698 }, 699 { 700 "empty intersection", 701 pgraph.Intersection(nil, 702 pgraph.Leaf(nil, 703 (DS("user", "user1", "...")), 704 (DS("user", "user2", "...")), 705 ), 706 pgraph.Leaf(nil, 707 (DS("user", "user3", "...")), 708 (DS("user", "user4", "...")), 709 ), 710 ), 711 }, 712 { 713 "simple exclusion", 714 pgraph.Exclusion(nil, 715 pgraph.Leaf(nil, 716 (DS("user", "user1", "...")), 717 (DS("user", "user2", "...")), 718 ), 719 pgraph.Leaf(nil, (DS("user", "user2", "..."))), 720 pgraph.Leaf(nil, (DS("user", "user3", "..."))), 721 ), 722 }, 723 { 724 "empty exclusion", 725 pgraph.Exclusion(nil, 726 pgraph.Leaf(nil, 727 (DS("user", "user1", "...")), 728 (DS("user", "user2", "...")), 729 ), 730 pgraph.Leaf(nil, (DS("user", "user1", "..."))), 731 pgraph.Leaf(nil, (DS("user", "user2", "..."))), 732 ), 733 }, 734 } 735 736 for _, tt := range table { 737 tt := tt 738 t.Run(tt.name, func(t *testing.T) { 739 out := v1svc.TranslateRelationshipTree(v1svc.TranslateExpansionTree(tt.input)) 740 require.Equal(t, tt.input, out) 741 }) 742 } 743 } 744 745 func TestLookupSubjects(t *testing.T) { 746 testCases := []struct { 747 resource *v1.ObjectReference 748 permission string 749 subjectType string 750 subjectRelation string 751 752 expectedSubjectIds []string 753 expectedErrorCode codes.Code 754 }{ 755 { 756 obj("document", "companyplan"), 757 "view", 758 "user", 759 "", 760 []string{"auditor", "legal", "owner"}, 761 codes.OK, 762 }, 763 { 764 obj("document", "healthplan"), 765 "view", 766 "user", 767 "", 768 []string{"chief_financial_officer"}, 769 codes.OK, 770 }, 771 { 772 obj("document", "masterplan"), 773 "view", 774 "user", 775 "", 776 []string{"auditor", "chief_financial_officer", "eng_lead", "legal", "owner", "product_manager", "vp_product"}, 777 codes.OK, 778 }, 779 { 780 obj("document", "masterplan"), 781 "view_and_edit", 782 "user", 783 "", 784 nil, 785 codes.OK, 786 }, 787 { 788 obj("document", "specialplan"), 789 "view_and_edit", 790 "user", 791 "", 792 []string{"multiroleguy"}, 793 codes.OK, 794 }, 795 { 796 obj("document", "unknownobj"), 797 "view", 798 "user", 799 "", 800 nil, 801 codes.OK, 802 }, 803 { 804 obj("document", "masterplan"), 805 "invalidperm", 806 "user", 807 "", 808 nil, 809 codes.FailedPrecondition, 810 }, 811 { 812 obj("document", "masterplan"), 813 "view", 814 "invalidsubtype", 815 "", 816 nil, 817 codes.FailedPrecondition, 818 }, 819 { 820 obj("unknown", "masterplan"), 821 "view", 822 "user", 823 "", 824 nil, 825 codes.FailedPrecondition, 826 }, 827 { 828 obj("document", "masterplan"), 829 "view", 830 "user", 831 "invalidrel", 832 nil, 833 codes.FailedPrecondition, 834 }, 835 { 836 obj("document", "specialplan"), 837 "*", 838 "user", 839 "", 840 nil, 841 codes.InvalidArgument, 842 }, 843 } 844 845 for _, delta := range testTimedeltas { 846 delta := delta 847 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 848 for _, tc := range testCases { 849 tc := tc 850 t.Run(fmt.Sprintf("%s:%s#%s for %s#%s", tc.resource.ObjectType, tc.resource.ObjectId, tc.permission, tc.subjectType, tc.subjectRelation), func(t *testing.T) { 851 require := require.New(t) 852 conn, cleanup, _, revision := testserver.NewTestServer(require, delta, memdb.DisableGC, true, tf.StandardDatastoreWithData) 853 client := v1.NewPermissionsServiceClient(conn) 854 t.Cleanup(func() { 855 goleak.VerifyNone(t, goleak.IgnoreCurrent()) 856 }) 857 t.Cleanup(cleanup) 858 859 var trailer metadata.MD 860 lookupClient, err := client.LookupSubjects(context.Background(), &v1.LookupSubjectsRequest{ 861 Resource: tc.resource, 862 Permission: tc.permission, 863 SubjectObjectType: tc.subjectType, 864 OptionalSubjectRelation: tc.subjectRelation, 865 Consistency: &v1.Consistency{ 866 Requirement: &v1.Consistency_AtLeastAsFresh{ 867 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 868 }, 869 }, 870 }, grpc.Trailer(&trailer)) 871 872 require.NoError(err) 873 if tc.expectedErrorCode == codes.OK { 874 var resolvedObjectIds []string 875 for { 876 resp, err := lookupClient.Recv() 877 if errors.Is(err, io.EOF) { 878 break 879 } 880 881 require.NoError(err) 882 883 resolvedObjectIds = append(resolvedObjectIds, resp.Subject.SubjectObjectId) 884 } 885 886 slices.Sort(tc.expectedSubjectIds) 887 slices.Sort(resolvedObjectIds) 888 889 require.Equal(tc.expectedSubjectIds, resolvedObjectIds) 890 891 dispatchCount, err := responsemeta.GetIntResponseTrailerMetadata(trailer, responsemeta.DispatchedOperationsCount) 892 require.NoError(err) 893 require.GreaterOrEqual(dispatchCount, 0) 894 } else { 895 _, err := lookupClient.Recv() 896 grpcutil.RequireStatus(t, tc.expectedErrorCode, err) 897 } 898 }) 899 } 900 }) 901 } 902 } 903 904 func TestCheckWithCaveats(t *testing.T) { 905 req := require.New(t) 906 conn, cleanup, _, revision := testserver.NewTestServer(req, testTimedeltas[0], memdb.DisableGC, true, tf.StandardDatastoreWithCaveatedData) 907 client := v1.NewPermissionsServiceClient(conn) 908 t.Cleanup(cleanup) 909 910 ctx := context.Background() 911 912 request := &v1.CheckPermissionRequest{ 913 Consistency: &v1.Consistency{ 914 Requirement: &v1.Consistency_AtLeastAsFresh{ 915 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 916 }, 917 }, 918 Resource: obj("document", "companyplan"), 919 Permission: "view", 920 Subject: sub("user", "owner", ""), 921 } 922 923 // caveat evaluated and returned false 924 var err error 925 request.Context, err = structpb.NewStruct(map[string]any{"secret": "incorrect_value"}) 926 req.NoError(err) 927 928 checkResp, err := client.CheckPermission(ctx, request) 929 req.NoError(err) 930 req.Equal(v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, checkResp.Permissionship) 931 932 // caveat evaluated and returned true 933 request.Context, err = structpb.NewStruct(map[string]any{"secret": "1234"}) 934 req.NoError(err) 935 936 checkResp, err = client.CheckPermission(ctx, request) 937 req.NoError(err) 938 req.Equal(v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, checkResp.Permissionship) 939 940 // caveat evaluated but context variable was missing 941 request.Context = nil 942 checkResp, err = client.CheckPermission(ctx, request) 943 req.NoError(err) 944 req.Equal(v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION, checkResp.Permissionship) 945 req.EqualValues([]string{"secret"}, checkResp.PartialCaveatInfo.MissingRequiredContext) 946 947 // context exceeds length limit 948 request.Context, err = structpb.NewStruct(generateMap(64)) 949 req.NoError(err) 950 951 _, err = client.CheckPermission(ctx, request) 952 grpcutil.RequireStatus(t, codes.InvalidArgument, err) 953 } 954 955 func TestCheckWithCaveatErrors(t *testing.T) { 956 req := require.New(t) 957 conn, cleanup, _, revision := testserver.NewTestServer( 958 req, 959 testTimedeltas[0], 960 memdb.DisableGC, 961 true, 962 func(ds datastore.Datastore, assertions *require.Assertions) (datastore.Datastore, datastore.Revision) { 963 return tf.DatastoreFromSchemaAndTestRelationships( 964 ds, 965 `definition user {} 966 967 caveat somecaveat(somemap map<any>) { 968 somemap.first == 42 && somemap.second < 56 969 } 970 971 definition document { 972 relation viewer: user with somecaveat 973 permission view = viewer 974 } 975 `, 976 []*core.RelationTuple{tuple.MustParse("document:firstdoc#viewer@user:tom[somecaveat]")}, 977 assertions, 978 ) 979 }) 980 981 client := v1.NewPermissionsServiceClient(conn) 982 t.Cleanup(cleanup) 983 984 ctx := context.Background() 985 986 tcs := []struct { 987 name string 988 context map[string]any 989 expectedError string 990 expectedCode codes.Code 991 }{ 992 { 993 "nil map in context", 994 map[string]any{ 995 "somemap": nil, 996 }, 997 "type error for parameters for caveat `somecaveat`: could not convert context parameter `somemap`: for map<any>: map requires a map, found: <nil>", 998 codes.InvalidArgument, 999 }, 1000 { 1001 "empty map in context", 1002 map[string]any{ 1003 "somemap": map[string]any{}, 1004 }, 1005 "evaluation error for caveat somecaveat: no such key: first", 1006 codes.InvalidArgument, 1007 }, 1008 { 1009 "wrong value in map", 1010 map[string]any{ 1011 "somemap": map[string]any{ 1012 "first": 42, 1013 "second": "hello", 1014 }, 1015 }, 1016 "evaluation error for caveat somecaveat: no such overload", 1017 codes.InvalidArgument, 1018 }, 1019 } 1020 1021 for _, tc := range tcs { 1022 tc := tc 1023 t.Run(tc.name, func(t *testing.T) { 1024 request := &v1.CheckPermissionRequest{ 1025 Consistency: &v1.Consistency{ 1026 Requirement: &v1.Consistency_AtLeastAsFresh{ 1027 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1028 }, 1029 }, 1030 Resource: obj("document", "firstdoc"), 1031 Permission: "view", 1032 Subject: sub("user", "tom", ""), 1033 } 1034 1035 var err error 1036 request.Context, err = structpb.NewStruct(tc.context) 1037 req.NoError(err) 1038 1039 _, err = client.CheckPermission(ctx, request) 1040 req.Error(err) 1041 req.Contains(err.Error(), tc.expectedError) 1042 grpcutil.RequireStatus(t, tc.expectedCode, err) 1043 }) 1044 } 1045 } 1046 1047 func TestLookupResourcesWithCaveats(t *testing.T) { 1048 req := require.New(t) 1049 conn, cleanup, _, revision := testserver.NewTestServer(req, testTimedeltas[0], memdb.DisableGC, true, 1050 func(ds datastore.Datastore, require *require.Assertions) (datastore.Datastore, datastore.Revision) { 1051 return tf.DatastoreFromSchemaAndTestRelationships(ds, ` 1052 definition user {} 1053 1054 caveat testcaveat(somecondition int) { 1055 somecondition == 42 1056 } 1057 1058 definition document { 1059 relation viewer: user | user with testcaveat 1060 permission view = viewer 1061 } 1062 `, []*core.RelationTuple{ 1063 tuple.MustParse("document:first#viewer@user:tom"), 1064 tuple.MustWithCaveat(tuple.MustParse("document:second#viewer@user:tom"), "testcaveat"), 1065 }, require) 1066 }) 1067 1068 client := v1.NewPermissionsServiceClient(conn) 1069 t.Cleanup(cleanup) 1070 1071 ctx := context.Background() 1072 1073 // Run with empty context. 1074 caveatContext, err := structpb.NewStruct(map[string]any{}) 1075 require.NoError(t, err) 1076 1077 request := &v1.LookupResourcesRequest{ 1078 Consistency: &v1.Consistency{ 1079 Requirement: &v1.Consistency_AtLeastAsFresh{ 1080 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1081 }, 1082 }, 1083 ResourceObjectType: "document", 1084 Permission: "view", 1085 Subject: sub("user", "tom", ""), 1086 Context: caveatContext, 1087 } 1088 1089 cli, err := client.LookupResources(ctx, request) 1090 req.NoError(err) 1091 1092 var responses []*v1.LookupResourcesResponse 1093 for { 1094 res, err := cli.Recv() 1095 if errors.Is(err, io.EOF) { 1096 break 1097 } 1098 1099 require.NoError(t, err) 1100 responses = append(responses, res) 1101 } 1102 1103 slices.SortFunc(responses, byIDAndPermission) 1104 1105 // NOTE: due to the order of the deduplication of dispatching in reachable resources, this can return the conditional 1106 // result more than once, as per cursored LR. Therefore, filter in that case. 1107 require.GreaterOrEqual(t, 3, len(responses)) 1108 require.LessOrEqual(t, 2, len(responses)) 1109 1110 require.Equal(t, "first", responses[0].ResourceObjectId) 1111 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, responses[0].Permissionship) 1112 1113 require.Equal(t, "second", responses[1].ResourceObjectId) 1114 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, responses[1].Permissionship) 1115 require.Equal(t, []string{"somecondition"}, responses[1].PartialCaveatInfo.MissingRequiredContext) 1116 1117 // Run with full context. 1118 caveatContext, err = structpb.NewStruct(map[string]any{ 1119 "somecondition": 42, 1120 }) 1121 require.NoError(t, err) 1122 1123 request = &v1.LookupResourcesRequest{ 1124 Consistency: &v1.Consistency{ 1125 Requirement: &v1.Consistency_AtLeastAsFresh{ 1126 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1127 }, 1128 }, 1129 ResourceObjectType: "document", 1130 Permission: "view", 1131 Subject: sub("user", "tom", ""), 1132 Context: caveatContext, 1133 } 1134 1135 cli, err = client.LookupResources(ctx, request) 1136 req.NoError(err) 1137 1138 responses = make([]*v1.LookupResourcesResponse, 0) 1139 for { 1140 res, err := cli.Recv() 1141 if errors.Is(err, io.EOF) { 1142 break 1143 } 1144 1145 require.NoError(t, err) 1146 responses = append(responses, res) 1147 } 1148 1149 require.Equal(t, 2, len(responses)) 1150 slices.SortFunc(responses, byIDAndPermission) 1151 1152 require.Equal(t, "first", responses[0].ResourceObjectId) // nolint: gosec 1153 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, responses[0].Permissionship) // nolint: gosec 1154 1155 require.Equal(t, "second", responses[1].ResourceObjectId) // nolint: gosec 1156 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_HAS_PERMISSION, responses[1].Permissionship) // nolint: gosec 1157 } 1158 1159 func byIDAndPermission(a, b *v1.LookupResourcesResponse) int { 1160 return strings.Compare( 1161 fmt.Sprintf("%s:%v", a.ResourceObjectId, a.Permissionship), 1162 fmt.Sprintf("%s:%v", b.ResourceObjectId, b.Permissionship), 1163 ) 1164 } 1165 1166 func TestLookupSubjectsWithCaveats(t *testing.T) { 1167 req := require.New(t) 1168 conn, cleanup, _, revision := testserver.NewTestServer(req, testTimedeltas[0], memdb.DisableGC, true, 1169 func(ds datastore.Datastore, require *require.Assertions) (datastore.Datastore, datastore.Revision) { 1170 return tf.DatastoreFromSchemaAndTestRelationships(ds, ` 1171 definition user {} 1172 1173 caveat testcaveat(somecondition int) { 1174 somecondition == 42 1175 } 1176 1177 definition document { 1178 relation viewer: user | user with testcaveat 1179 permission view = viewer 1180 } 1181 `, []*core.RelationTuple{ 1182 tuple.MustParse("document:first#viewer@user:tom"), 1183 tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:sarah"), "testcaveat"), 1184 }, require) 1185 }) 1186 1187 client := v1.NewPermissionsServiceClient(conn) 1188 t.Cleanup(cleanup) 1189 1190 ctx := context.Background() 1191 1192 // Call with empty context. 1193 caveatContext, err := structpb.NewStruct(map[string]any{}) 1194 req.NoError(err) 1195 1196 request := &v1.LookupSubjectsRequest{ 1197 Consistency: &v1.Consistency{ 1198 Requirement: &v1.Consistency_AtLeastAsFresh{ 1199 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1200 }, 1201 }, 1202 Resource: obj("document", "first"), 1203 Permission: "view", 1204 SubjectObjectType: "user", 1205 Context: caveatContext, 1206 } 1207 1208 lookupClient, err := client.LookupSubjects(ctx, request) 1209 req.NoError(err) 1210 1211 var resolvedSubjects []expectedSubject 1212 for { 1213 resp, err := lookupClient.Recv() 1214 if errors.Is(err, io.EOF) { 1215 break 1216 } 1217 1218 require.NoError(t, err) 1219 resolvedSubjects = append(resolvedSubjects, expectedSubject{ 1220 resp.Subject.SubjectObjectId, 1221 resp.Subject.Permissionship == v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 1222 }) 1223 } 1224 1225 expectedSubjects := []expectedSubject{ 1226 {"sarah", true}, 1227 {"tom", false}, 1228 } 1229 1230 slices.SortFunc(resolvedSubjects, bySubjectID) 1231 slices.SortFunc(expectedSubjects, bySubjectID) 1232 1233 req.Equal(expectedSubjects, resolvedSubjects) 1234 1235 // Call with proper context. 1236 caveatContext, err = structpb.NewStruct(map[string]any{ 1237 "somecondition": 42, 1238 }) 1239 req.NoError(err) 1240 1241 request = &v1.LookupSubjectsRequest{ 1242 Consistency: &v1.Consistency{ 1243 Requirement: &v1.Consistency_AtLeastAsFresh{ 1244 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1245 }, 1246 }, 1247 Resource: obj("document", "first"), 1248 Permission: "view", 1249 SubjectObjectType: "user", 1250 Context: caveatContext, 1251 } 1252 1253 lookupClient, err = client.LookupSubjects(ctx, request) 1254 req.NoError(err) 1255 1256 resolvedSubjects = []expectedSubject{} 1257 for { 1258 resp, err := lookupClient.Recv() 1259 if errors.Is(err, io.EOF) { 1260 break 1261 } 1262 1263 require.NoError(t, err) 1264 resolvedSubjects = append(resolvedSubjects, expectedSubject{ 1265 resp.Subject.SubjectObjectId, 1266 resp.Subject.Permissionship == v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 1267 }) 1268 } 1269 1270 expectedSubjects = []expectedSubject{ 1271 {"sarah", false}, 1272 {"tom", false}, 1273 } 1274 1275 slices.SortFunc(resolvedSubjects, bySubjectID) 1276 slices.SortFunc(expectedSubjects, bySubjectID) 1277 1278 req.Equal(expectedSubjects, resolvedSubjects) 1279 1280 // Call with negative context. 1281 caveatContext, err = structpb.NewStruct(map[string]any{ 1282 "somecondition": 32, 1283 }) 1284 req.NoError(err) 1285 1286 request = &v1.LookupSubjectsRequest{ 1287 Consistency: &v1.Consistency{ 1288 Requirement: &v1.Consistency_AtLeastAsFresh{ 1289 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1290 }, 1291 }, 1292 Resource: obj("document", "first"), 1293 Permission: "view", 1294 SubjectObjectType: "user", 1295 Context: caveatContext, 1296 } 1297 1298 lookupClient, err = client.LookupSubjects(ctx, request) 1299 req.NoError(err) 1300 1301 resolvedSubjects = []expectedSubject{} 1302 for { 1303 resp, err := lookupClient.Recv() 1304 if errors.Is(err, io.EOF) { 1305 break 1306 } 1307 1308 require.NoError(t, err) 1309 resolvedSubjects = append(resolvedSubjects, expectedSubject{ 1310 resp.Subject.SubjectObjectId, 1311 resp.Subject.Permissionship == v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 1312 }) 1313 } 1314 1315 expectedSubjects = []expectedSubject{ 1316 {"tom", false}, 1317 } 1318 1319 slices.SortFunc(resolvedSubjects, bySubjectID) 1320 slices.SortFunc(expectedSubjects, bySubjectID) 1321 1322 req.Equal(expectedSubjects, resolvedSubjects) 1323 } 1324 1325 func TestLookupSubjectsWithCaveatedWildcards(t *testing.T) { 1326 req := require.New(t) 1327 conn, cleanup, _, revision := testserver.NewTestServer(req, testTimedeltas[0], memdb.DisableGC, true, 1328 func(ds datastore.Datastore, require *require.Assertions) (datastore.Datastore, datastore.Revision) { 1329 return tf.DatastoreFromSchemaAndTestRelationships(ds, ` 1330 definition user {} 1331 1332 caveat testcaveat(somecondition int) { 1333 somecondition == 42 1334 } 1335 1336 caveat anothercaveat(anothercondition int) { 1337 anothercondition == 42 1338 } 1339 1340 definition document { 1341 relation viewer: user:* with testcaveat 1342 relation banned: user with testcaveat 1343 permission view = viewer - banned 1344 } 1345 `, []*core.RelationTuple{ 1346 tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:*"), "testcaveat"), 1347 tuple.MustWithCaveat(tuple.MustParse("document:first#banned@user:bannedguy"), "anothercaveat"), 1348 }, require) 1349 }) 1350 1351 client := v1.NewPermissionsServiceClient(conn) 1352 t.Cleanup(cleanup) 1353 1354 ctx := context.Background() 1355 1356 // Call with empty context. 1357 caveatContext, err := structpb.NewStruct(map[string]any{}) 1358 req.NoError(err) 1359 1360 request := &v1.LookupSubjectsRequest{ 1361 Consistency: &v1.Consistency{ 1362 Requirement: &v1.Consistency_AtLeastAsFresh{ 1363 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1364 }, 1365 }, 1366 Resource: obj("document", "first"), 1367 Permission: "view", 1368 SubjectObjectType: "user", 1369 Context: caveatContext, 1370 } 1371 1372 lookupClient, err := client.LookupSubjects(ctx, request) 1373 req.NoError(err) 1374 1375 found := false 1376 for { 1377 resp, err := lookupClient.Recv() 1378 if errors.Is(err, io.EOF) { 1379 break 1380 } 1381 1382 found = true 1383 require.NoError(t, err) 1384 require.Equal(t, "*", resp.Subject.SubjectObjectId) 1385 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resp.Subject.Permissionship) 1386 require.Equal(t, 1, len(resp.ExcludedSubjects)) 1387 1388 require.Equal(t, "bannedguy", resp.ExcludedSubjects[0].SubjectObjectId) 1389 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resp.ExcludedSubjects[0].Permissionship) 1390 } 1391 require.True(t, found) 1392 1393 // Call with negative context. 1394 caveatContext, err = structpb.NewStruct(map[string]any{ 1395 "anothercondition": 41, 1396 }) 1397 req.NoError(err) 1398 1399 request = &v1.LookupSubjectsRequest{ 1400 Consistency: &v1.Consistency{ 1401 Requirement: &v1.Consistency_AtLeastAsFresh{ 1402 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1403 }, 1404 }, 1405 Resource: obj("document", "first"), 1406 Permission: "view", 1407 SubjectObjectType: "user", 1408 Context: caveatContext, 1409 } 1410 1411 lookupClient, err = client.LookupSubjects(ctx, request) 1412 req.NoError(err) 1413 1414 found = false 1415 for { 1416 resp, err := lookupClient.Recv() 1417 if errors.Is(err, io.EOF) { 1418 break 1419 } 1420 1421 found = true 1422 require.NoError(t, err) 1423 require.Equal(t, "*", resp.Subject.SubjectObjectId) 1424 require.Equal(t, v1.LookupPermissionship_LOOKUP_PERMISSIONSHIP_CONDITIONAL_PERMISSION, resp.Subject.Permissionship) 1425 require.Equal(t, 0, len(resp.ExcludedSubjects)) 1426 } 1427 require.True(t, found) 1428 } 1429 1430 type expectedSubject struct { 1431 subjectID string 1432 isConditional bool 1433 } 1434 1435 func bySubjectID(a, b expectedSubject) int { 1436 return cmp.Compare(a.subjectID, b.subjectID) 1437 } 1438 1439 func generateMap(length int) map[string]any { 1440 output := make(map[string]any, length) 1441 for i := 0; i < length; i++ { 1442 random := randString(32) 1443 output[random] = random 1444 } 1445 return output 1446 } 1447 1448 var randInput = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 1449 1450 func randString(length int) string { 1451 b := make([]rune, length) 1452 for i := range b { 1453 b[i] = randInput[rand.Intn(len(randInput))] //nolint:gosec 1454 } 1455 return string(b) 1456 } 1457 1458 func TestGetCaveatContext(t *testing.T) { 1459 strct, err := structpb.NewStruct(map[string]any{"foo": "bar"}) 1460 require.NoError(t, err) 1461 1462 _, err = v1svc.GetCaveatContext(context.Background(), strct, 1) 1463 require.ErrorContains(t, err, "request caveat context should have less than 1 bytes") 1464 1465 caveatMap, err := v1svc.GetCaveatContext(context.Background(), strct, 0) 1466 require.NoError(t, err) 1467 require.Contains(t, caveatMap, "foo") 1468 1469 caveatMap, err = v1svc.GetCaveatContext(context.Background(), strct, -1) 1470 require.NoError(t, err) 1471 require.Contains(t, caveatMap, "foo") 1472 } 1473 1474 func TestLookupResourcesWithCursors(t *testing.T) { 1475 testCases := []struct { 1476 objectType string 1477 permission string 1478 subject *v1.SubjectReference 1479 expectedObjectIds []string 1480 }{ 1481 { 1482 "document", "view", 1483 sub("user", "eng_lead", ""), 1484 []string{"masterplan"}, 1485 }, 1486 { 1487 "document", "view", 1488 sub("user", "product_manager", ""), 1489 []string{"masterplan"}, 1490 }, 1491 { 1492 "document", "view", 1493 sub("user", "chief_financial_officer", ""), 1494 []string{"masterplan", "healthplan"}, 1495 }, 1496 { 1497 "document", "view", 1498 sub("user", "auditor", ""), 1499 []string{"masterplan", "companyplan"}, 1500 }, 1501 { 1502 "document", "view", 1503 sub("user", "vp_product", ""), 1504 []string{"masterplan"}, 1505 }, 1506 { 1507 "document", "view", 1508 sub("user", "legal", ""), 1509 []string{"masterplan", "companyplan"}, 1510 }, 1511 { 1512 "document", "view", 1513 sub("user", "owner", ""), 1514 []string{"masterplan", "companyplan", "ownerplan"}, 1515 }, 1516 } 1517 1518 for _, delta := range testTimedeltas { 1519 delta := delta 1520 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 1521 for _, limit := range []int{1, 2, 5, 10, 100} { 1522 limit := limit 1523 t.Run(fmt.Sprintf("limit%d", limit), func(t *testing.T) { 1524 for _, tc := range testCases { 1525 tc := tc 1526 t.Run(fmt.Sprintf("%s::%s from %s:%s#%s", tc.objectType, tc.permission, tc.subject.Object.ObjectType, tc.subject.Object.ObjectId, tc.subject.OptionalRelation), func(t *testing.T) { 1527 require := require.New(t) 1528 conn, cleanup, _, revision := testserver.NewTestServer(require, delta, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1529 client := v1.NewPermissionsServiceClient(conn) 1530 t.Cleanup(func() { 1531 goleak.VerifyNone(t, goleak.IgnoreCurrent()) 1532 }) 1533 t.Cleanup(cleanup) 1534 1535 var currentCursor *v1.Cursor 1536 foundObjectIds := mapz.NewSet[string]() 1537 1538 for i := 0; i < 5; i++ { 1539 var trailer metadata.MD 1540 lookupClient, err := client.LookupResources(context.Background(), &v1.LookupResourcesRequest{ 1541 ResourceObjectType: tc.objectType, 1542 Permission: tc.permission, 1543 Subject: tc.subject, 1544 Consistency: &v1.Consistency{ 1545 Requirement: &v1.Consistency_AtLeastAsFresh{ 1546 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1547 }, 1548 }, 1549 OptionalLimit: uint32(limit), 1550 OptionalCursor: currentCursor, 1551 }, grpc.Trailer(&trailer)) 1552 1553 require.NoError(err) 1554 1555 var locallyResolvedObjectIds []string 1556 for { 1557 resp, err := lookupClient.Recv() 1558 if errors.Is(err, io.EOF) { 1559 break 1560 } 1561 1562 require.NoError(err) 1563 1564 locallyResolvedObjectIds = append(locallyResolvedObjectIds, resp.ResourceObjectId) 1565 foundObjectIds.Add(resp.ResourceObjectId) 1566 currentCursor = resp.AfterResultCursor 1567 } 1568 1569 require.LessOrEqual(len(locallyResolvedObjectIds), limit) 1570 if len(locallyResolvedObjectIds) < limit { 1571 break 1572 } 1573 } 1574 1575 resolvedObjectIds := foundObjectIds.AsSlice() 1576 slices.Sort(tc.expectedObjectIds) 1577 slices.Sort(resolvedObjectIds) 1578 1579 require.Equal(tc.expectedObjectIds, resolvedObjectIds) 1580 }) 1581 } 1582 }) 1583 } 1584 }) 1585 } 1586 } 1587 1588 func TestLookupResourcesDeduplication(t *testing.T) { 1589 req := require.New(t) 1590 conn, cleanup, _, revision := testserver.NewTestServer(req, testTimedeltas[0], memdb.DisableGC, true, 1591 func(ds datastore.Datastore, require *require.Assertions) (datastore.Datastore, datastore.Revision) { 1592 return tf.DatastoreFromSchemaAndTestRelationships(ds, ` 1593 definition user {} 1594 1595 definition document { 1596 relation viewer: user 1597 relation editor: user 1598 permission view = viewer + editor 1599 } 1600 `, []*core.RelationTuple{ 1601 tuple.MustParse("document:first#viewer@user:tom"), 1602 tuple.MustParse("document:first#editor@user:tom"), 1603 }, require) 1604 }) 1605 1606 client := v1.NewPermissionsServiceClient(conn) 1607 t.Cleanup(cleanup) 1608 1609 lookupClient, err := client.LookupResources(context.Background(), &v1.LookupResourcesRequest{ 1610 ResourceObjectType: "document", 1611 Permission: "view", 1612 Subject: sub("user", "tom", ""), 1613 Consistency: &v1.Consistency{ 1614 Requirement: &v1.Consistency_AtLeastAsFresh{ 1615 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 1616 }, 1617 }, 1618 }) 1619 1620 require.NoError(t, err) 1621 1622 foundObjectIds := mapz.NewSet[string]() 1623 for { 1624 resp, err := lookupClient.Recv() 1625 if errors.Is(err, io.EOF) { 1626 break 1627 } 1628 1629 require.NoError(t, err) 1630 require.True(t, foundObjectIds.Add(resp.ResourceObjectId)) 1631 } 1632 1633 require.Equal(t, []string{"first"}, foundObjectIds.AsSlice()) 1634 } 1635 1636 func TestLookupResourcesBeyondAllowedLimit(t *testing.T) { 1637 require := require.New(t) 1638 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1639 client := v1.NewPermissionsServiceClient(conn) 1640 t.Cleanup(cleanup) 1641 1642 resp, err := client.LookupResources(context.Background(), &v1.LookupResourcesRequest{ 1643 ResourceObjectType: "document", 1644 Permission: "view", 1645 Subject: sub("user", "tom", ""), 1646 OptionalLimit: 1005, 1647 }) 1648 require.NoError(err) 1649 1650 _, err = resp.Recv() 1651 require.Error(err) 1652 require.Contains(err.Error(), "provided limit 1005 is greater than maximum allowed of 1000") 1653 } 1654 1655 func TestCheckBulkPermissions(t *testing.T) { 1656 defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) 1657 1658 conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.StandardDatastoreWithCaveatedData) 1659 client := v1.NewPermissionsServiceClient(conn) 1660 defer cleanup() 1661 1662 testCases := []struct { 1663 name string 1664 requests []string 1665 response []bulkCheckTest 1666 expectedDispatchCount int 1667 }{ 1668 { 1669 name: "same resource and permission, different subjects", 1670 requests: []string{ 1671 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1672 `document:masterplan#view@user:product_manager[test:{"secret": "1234"}]`, 1673 `document:masterplan#view@user:villain[test:{"secret": "1234"}]`, 1674 }, 1675 response: []bulkCheckTest{ 1676 { 1677 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1678 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 1679 }, 1680 { 1681 req: `document:masterplan#view@user:product_manager[test:{"secret": "1234"}]`, 1682 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 1683 }, 1684 { 1685 req: `document:masterplan#view@user:villain[test:{"secret": "1234"}]`, 1686 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 1687 }, 1688 }, 1689 expectedDispatchCount: 49, 1690 }, 1691 { 1692 name: "different resources, same permission and subject", 1693 requests: []string{ 1694 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1695 `document:companyplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1696 `document:healthplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1697 }, 1698 response: []bulkCheckTest{ 1699 { 1700 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1701 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 1702 }, 1703 { 1704 req: `document:companyplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1705 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 1706 }, 1707 { 1708 req: `document:healthplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1709 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 1710 }, 1711 }, 1712 expectedDispatchCount: 18, 1713 }, 1714 { 1715 name: "some items fail", 1716 requests: []string{ 1717 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1718 "fake:fake#fake@fake:fake", 1719 "superfake:plan#view@user:eng_lead", 1720 }, 1721 response: []bulkCheckTest{ 1722 { 1723 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1724 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 1725 }, 1726 { 1727 req: "fake:fake#fake@fake:fake", 1728 err: namespace.NewNamespaceNotFoundErr("fake"), 1729 }, 1730 { 1731 req: "superfake:plan#view@user:eng_lead", 1732 err: namespace.NewNamespaceNotFoundErr("superfake"), 1733 }, 1734 }, 1735 expectedDispatchCount: 17, 1736 }, 1737 { 1738 name: "different caveat context is not clustered", 1739 requests: []string{ 1740 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1741 `document:companyplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1742 `document:masterplan#view@user:eng_lead[test:{"secret": "4321"}]`, 1743 `document:masterplan#view@user:eng_lead`, 1744 }, 1745 response: []bulkCheckTest{ 1746 { 1747 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1748 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 1749 }, 1750 { 1751 req: `document:companyplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1752 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 1753 }, 1754 { 1755 req: `document:masterplan#view@user:eng_lead[test:{"secret": "4321"}]`, 1756 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 1757 }, 1758 { 1759 req: `document:masterplan#view@user:eng_lead`, 1760 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 1761 partial: []string{"secret"}, 1762 }, 1763 }, 1764 expectedDispatchCount: 50, 1765 }, 1766 { 1767 name: "namespace validation", 1768 requests: []string{ 1769 "document:masterplan#view@fake:fake", 1770 "fake:fake#fake@user:eng_lead", 1771 }, 1772 response: []bulkCheckTest{ 1773 { 1774 req: "document:masterplan#view@fake:fake", 1775 err: namespace.NewNamespaceNotFoundErr("fake"), 1776 }, 1777 { 1778 req: "fake:fake#fake@user:eng_lead", 1779 err: namespace.NewNamespaceNotFoundErr("fake"), 1780 }, 1781 }, 1782 expectedDispatchCount: 1, 1783 }, 1784 { 1785 name: "chunking test", 1786 requests: (func() []string { 1787 toReturn := make([]string, 0, datastore.FilterMaximumIDCount+5) 1788 for i := 0; i < int(datastore.FilterMaximumIDCount+5); i++ { 1789 toReturn = append(toReturn, fmt.Sprintf(`document:masterplan-%d#view@user:eng_lead`, i)) 1790 } 1791 1792 return toReturn 1793 })(), 1794 response: (func() []bulkCheckTest { 1795 toReturn := make([]bulkCheckTest, 0, datastore.FilterMaximumIDCount+5) 1796 for i := 0; i < int(datastore.FilterMaximumIDCount+5); i++ { 1797 toReturn = append(toReturn, bulkCheckTest{ 1798 req: fmt.Sprintf(`document:masterplan-%d#view@user:eng_lead`, i), 1799 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 1800 }) 1801 } 1802 1803 return toReturn 1804 })(), 1805 expectedDispatchCount: 11, 1806 }, 1807 { 1808 name: "chunking test with errors", 1809 requests: (func() []string { 1810 toReturn := make([]string, 0, datastore.FilterMaximumIDCount+6) 1811 toReturn = append(toReturn, `nondoc:masterplan#view@user:eng_lead`) 1812 1813 for i := 0; i < int(datastore.FilterMaximumIDCount+5); i++ { 1814 toReturn = append(toReturn, fmt.Sprintf(`document:masterplan-%d#view@user:eng_lead`, i)) 1815 } 1816 1817 return toReturn 1818 })(), 1819 response: (func() []bulkCheckTest { 1820 toReturn := make([]bulkCheckTest, 0, datastore.FilterMaximumIDCount+6) 1821 toReturn = append(toReturn, bulkCheckTest{ 1822 req: `nondoc:masterplan#view@user:eng_lead`, 1823 err: namespace.NewNamespaceNotFoundErr("nondoc"), 1824 }) 1825 1826 for i := 0; i < int(datastore.FilterMaximumIDCount+5); i++ { 1827 toReturn = append(toReturn, bulkCheckTest{ 1828 req: fmt.Sprintf(`document:masterplan-%d#view@user:eng_lead`, i), 1829 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 1830 }) 1831 } 1832 1833 return toReturn 1834 })(), 1835 expectedDispatchCount: 11, 1836 }, 1837 { 1838 name: "same resource and permission with same subject, repeated", 1839 requests: []string{ 1840 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1841 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1842 }, 1843 response: []bulkCheckTest{ 1844 { 1845 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1846 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 1847 }, 1848 { 1849 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 1850 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 1851 }, 1852 }, 1853 expectedDispatchCount: 17, 1854 }, 1855 } 1856 1857 for _, tt := range testCases { 1858 tt := tt 1859 t.Run(tt.name, func(t *testing.T) { 1860 req := v1.CheckBulkPermissionsRequest{ 1861 Consistency: &v1.Consistency{ 1862 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 1863 }, 1864 Items: make([]*v1.CheckBulkPermissionsRequestItem, 0, len(tt.requests)), 1865 } 1866 1867 for _, r := range tt.requests { 1868 req.Items = append(req.Items, relToCheckBulkRequestItem(r)) 1869 } 1870 1871 expected := make([]*v1.CheckBulkPermissionsPair, 0, len(tt.response)) 1872 for _, r := range tt.response { 1873 reqRel := tuple.ParseRel(r.req) 1874 resp := &v1.CheckBulkPermissionsPair_Item{ 1875 Item: &v1.CheckBulkPermissionsResponseItem{ 1876 Permissionship: r.resp, 1877 }, 1878 } 1879 pair := &v1.CheckBulkPermissionsPair{ 1880 Request: &v1.CheckBulkPermissionsRequestItem{ 1881 Resource: reqRel.Resource, 1882 Permission: reqRel.Relation, 1883 Subject: reqRel.Subject, 1884 }, 1885 Response: resp, 1886 } 1887 if reqRel.OptionalCaveat != nil { 1888 pair.Request.Context = reqRel.OptionalCaveat.Context 1889 } 1890 if len(r.partial) > 0 { 1891 resp.Item.PartialCaveatInfo = &v1.PartialCaveatInfo{ 1892 MissingRequiredContext: r.partial, 1893 } 1894 } 1895 1896 if r.err != nil { 1897 rewritten := shared.RewriteError(context.Background(), r.err, &shared.ConfigForErrors{}) 1898 s, ok := status.FromError(rewritten) 1899 require.True(t, ok, "expected provided error to be status") 1900 pair.Response = &v1.CheckBulkPermissionsPair_Error{ 1901 Error: s.Proto(), 1902 } 1903 } 1904 expected = append(expected, pair) 1905 } 1906 1907 var trailer metadata.MD 1908 actual, err := client.CheckBulkPermissions(context.Background(), &req, grpc.Trailer(&trailer)) 1909 require.NoError(t, err) 1910 1911 dispatchCount, err := responsemeta.GetIntResponseTrailerMetadata(trailer, responsemeta.DispatchedOperationsCount) 1912 require.NoError(t, err) 1913 require.Equal(t, tt.expectedDispatchCount, dispatchCount) 1914 1915 testutil.RequireProtoSlicesEqual(t, expected, actual.Pairs, nil, "response bulk check pairs did not match") 1916 }) 1917 } 1918 } 1919 1920 func TestLookupSubjectsWithCursors(t *testing.T) { 1921 testCases := []struct { 1922 resource *v1.ObjectReference 1923 permission string 1924 subjectType string 1925 subjectRelation string 1926 1927 expectedSubjectIds []string 1928 }{ 1929 { 1930 obj("document", "companyplan"), 1931 "view", 1932 "user", 1933 "", 1934 []string{"auditor", "legal", "owner"}, 1935 }, 1936 { 1937 obj("document", "healthplan"), 1938 "view", 1939 "user", 1940 "", 1941 []string{"chief_financial_officer"}, 1942 }, 1943 { 1944 obj("document", "masterplan"), 1945 "view", 1946 "user", 1947 "", 1948 []string{"auditor", "chief_financial_officer", "eng_lead", "legal", "owner", "product_manager", "vp_product"}, 1949 }, 1950 { 1951 obj("document", "masterplan"), 1952 "view_and_edit", 1953 "user", 1954 "", 1955 nil, 1956 }, 1957 { 1958 obj("document", "specialplan"), 1959 "view_and_edit", 1960 "user", 1961 "", 1962 []string{"multiroleguy"}, 1963 }, 1964 { 1965 obj("document", "unknownobj"), 1966 "view", 1967 "user", 1968 "", 1969 nil, 1970 }, 1971 } 1972 1973 for _, delta := range testTimedeltas { 1974 delta := delta 1975 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 1976 for _, limit := range []int{1, 2, 5, 10, 100} { 1977 limit := limit 1978 t.Run(fmt.Sprintf("limit%d_", limit), func(t *testing.T) { 1979 for _, tc := range testCases { 1980 tc := tc 1981 t.Run(fmt.Sprintf("%s:%s#%s for %s#%s", tc.resource.ObjectType, tc.resource.ObjectId, tc.permission, tc.subjectType, tc.subjectRelation), func(t *testing.T) { 1982 require := require.New(t) 1983 conn, cleanup, _, revision := testserver.NewTestServer(require, delta, memdb.DisableGC, true, tf.StandardDatastoreWithData) 1984 client := v1.NewPermissionsServiceClient(conn) 1985 t.Cleanup(func() { 1986 goleak.VerifyNone(t, goleak.IgnoreCurrent()) 1987 }) 1988 t.Cleanup(cleanup) 1989 1990 var currentCursor *v1.Cursor 1991 foundObjectIds := mapz.NewSet[string]() 1992 1993 for i := 0; i < 15; i++ { 1994 var trailer metadata.MD 1995 lookupClient, err := client.LookupSubjects(context.Background(), &v1.LookupSubjectsRequest{ 1996 Resource: tc.resource, 1997 Permission: tc.permission, 1998 SubjectObjectType: tc.subjectType, 1999 OptionalSubjectRelation: tc.subjectRelation, 2000 Consistency: &v1.Consistency{ 2001 Requirement: &v1.Consistency_AtLeastAsFresh{ 2002 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 2003 }, 2004 }, 2005 OptionalConcreteLimit: uint32(limit), 2006 OptionalCursor: currentCursor, 2007 }, grpc.Trailer(&trailer)) 2008 2009 require.NoError(err) 2010 var resolvedObjectIds []string 2011 existingCursor := currentCursor 2012 for { 2013 resp, err := lookupClient.Recv() 2014 if errors.Is(err, io.EOF) { 2015 break 2016 } 2017 2018 require.NoError(err) 2019 2020 resolvedObjectIds = append(resolvedObjectIds, resp.Subject.SubjectObjectId) 2021 foundObjectIds.Add(resp.Subject.SubjectObjectId) 2022 currentCursor = resp.AfterResultCursor 2023 } 2024 2025 require.LessOrEqual(len(resolvedObjectIds), limit, "starting at cursor %v, found: %v", existingCursor, resolvedObjectIds) 2026 2027 dispatchCount, err := responsemeta.GetIntResponseTrailerMetadata(trailer, responsemeta.DispatchedOperationsCount) 2028 require.NoError(err) 2029 require.GreaterOrEqual(dispatchCount, 0) 2030 2031 if len(resolvedObjectIds) == 0 { 2032 break 2033 } 2034 } 2035 2036 allResolvedObjectIds := foundObjectIds.AsSlice() 2037 2038 sort.Strings(tc.expectedSubjectIds) 2039 sort.Strings(allResolvedObjectIds) 2040 2041 require.Equal(tc.expectedSubjectIds, allResolvedObjectIds) 2042 }) 2043 } 2044 }) 2045 } 2046 }) 2047 } 2048 } 2049 2050 func relToCheckBulkRequestItem(rel string) *v1.CheckBulkPermissionsRequestItem { 2051 r := tuple.ParseRel(rel) 2052 item := &v1.CheckBulkPermissionsRequestItem{ 2053 Resource: r.Resource, 2054 Permission: r.Relation, 2055 Subject: r.Subject, 2056 } 2057 if r.OptionalCaveat != nil { 2058 item.Context = r.OptionalCaveat.Context 2059 } 2060 return item 2061 } 2062 2063 func withNeedsCaveatContexts(ids []string, contextKeys ...string) []string { 2064 for i := range ids { 2065 sort.Strings(contextKeys) 2066 ids[i] = fmt.Sprintf("%s needs [%s]", ids[i], strings.Join(contextKeys, ",")) 2067 } 2068 return ids 2069 } 2070 2071 func TestLookupSubjectsWithCursorsOverSchema(t *testing.T) { 2072 testCases := []struct { 2073 name string 2074 schema string 2075 relationships []*core.RelationTuple 2076 2077 resource *v1.ObjectReference 2078 permission string 2079 subjectType string 2080 subjectRelation string 2081 2082 expectedSubjectIds []string 2083 }{ 2084 { 2085 "basic lookup", 2086 ` 2087 definition user {} 2088 2089 definition document { 2090 relation viewer: user 2091 permission view = viewer 2092 } 2093 `, 2094 itestutil.GenResourceTuples("document", "somedoc", "viewer", "user", "...", 1000), 2095 2096 obj("document", "somedoc"), 2097 "view", 2098 "user", 2099 "", 2100 itestutil.GenSubjectIds("user", 1000), 2101 }, 2102 { 2103 "basic resolved caveated lookup", 2104 ` 2105 definition user {} 2106 2107 caveat testcaveat(somecondition int) { 2108 somecondition == 42 2109 } 2110 2111 definition document { 2112 relation viewer: user with testcaveat 2113 permission view = viewer 2114 } 2115 `, 2116 itestutil.GenResourceTuplesWithCaveat("document", "somedoc", "viewer", "user", "...", "testcaveat", map[string]any{"somecondition": 42}, 1000), 2117 2118 obj("document", "somedoc"), 2119 "view", 2120 "user", 2121 "", 2122 itestutil.GenSubjectIds("user", 1000), 2123 }, 2124 { 2125 "basic unresolved caveated lookup", 2126 ` 2127 definition user {} 2128 2129 caveat testcaveat(somecondition int) { 2130 somecondition == 42 2131 } 2132 2133 definition document { 2134 relation viewer: user with testcaveat 2135 permission view = viewer 2136 } 2137 `, 2138 itestutil.GenResourceTuplesWithCaveat("document", "somedoc", "viewer", "user", "...", "testcaveat", map[string]any{}, 1000), 2139 2140 obj("document", "somedoc"), 2141 "view", 2142 "user", 2143 "", 2144 withNeedsCaveatContexts(itestutil.GenSubjectIds("user", 1000), "somecondition"), 2145 }, 2146 { 2147 "partially resolved caveat lookup", 2148 ` 2149 definition user {} 2150 2151 caveat testcaveat(somecondition int) { 2152 somecondition == 42 2153 } 2154 2155 definition document { 2156 relation viewer: user | user with testcaveat 2157 permission view = viewer 2158 } 2159 `, 2160 []*core.RelationTuple{ 2161 tuple.MustParse("document:somedoc#viewer@user:tom"), 2162 tuple.MustParse("document:somedoc#viewer@user:fred[testcaveat:{\"somecondition\":42}]"), 2163 tuple.MustParse("document:somedoc#viewer@user:sam[testcaveat:{\"somecondition\":41}]"), 2164 tuple.MustParse("document:somedoc#viewer@user:sarah[testcaveat]"), 2165 }, 2166 2167 obj("document", "somedoc"), 2168 "view", 2169 "user", 2170 "", 2171 []string{"tom", "fred", "sarah needs [somecondition]"}, 2172 }, 2173 { 2174 "lookup over wildcard", 2175 ` 2176 definition user {} 2177 2178 definition document { 2179 relation viewer: user | user:* 2180 permission view = viewer 2181 } 2182 `, 2183 itestutil.JoinTuples( 2184 itestutil.GenResourceTuples("document", "somedoc", "viewer", "user", "...", 500), 2185 []*core.RelationTuple{ 2186 tuple.MustParse("document:somedoc#viewer@user:*"), 2187 }, 2188 ), 2189 2190 obj("document", "somedoc"), 2191 "view", 2192 "user", 2193 "", 2194 2195 append(itestutil.GenSubjectIds("user", 500), "*"), 2196 }, 2197 { 2198 "lookup over wildcard with exclusions", 2199 ` 2200 definition user {} 2201 2202 definition document { 2203 relation viewer: user | user:* 2204 relation banned: user 2205 permission view = viewer - banned 2206 } 2207 `, 2208 2209 itestutil.JoinTuples( 2210 itestutil.GenResourceTuples("document", "somedoc", "banned", "user", "...", 25), 2211 []*core.RelationTuple{ 2212 tuple.MustParse("document:somedoc#viewer@user:*"), 2213 }, 2214 ), 2215 2216 obj("document", "somedoc"), 2217 "view", 2218 "user", 2219 2220 "", 2221 []string{ 2222 "*", 2223 "!user-0", "!user-1", "!user-2", "!user-3", 2224 "!user-4", "!user-5", "!user-6", "!user-7", 2225 "!user-8", "!user-9", "!user-10", "!user-11", 2226 "!user-12", "!user-13", "!user-14", "!user-15", 2227 "!user-16", "!user-17", "!user-18", "!user-19", 2228 "!user-20", "!user-21", "!user-22", "!user-23", 2229 "!user-24", 2230 }, 2231 }, 2232 { 2233 "lookup over wildcard with caveated exclusions", 2234 ` 2235 definition user {} 2236 2237 caveat testcaveat(somecondition int) { 2238 somecondition == 42 2239 } 2240 2241 definition document { 2242 relation viewer: user | user:* 2243 relation banned: user with testcaveat 2244 permission view = viewer - banned 2245 } 2246 `, 2247 2248 itestutil.JoinTuples( 2249 []*core.RelationTuple{ 2250 tuple.MustParse("document:somedoc#viewer@user:*"), 2251 tuple.MustParse("document:somedoc#banned@user:tom"), 2252 tuple.MustParse("document:somedoc#banned@user:fred[testcaveat:{\"somecondition\":42}]"), 2253 tuple.MustParse("document:somedoc#banned@user:sam[testcaveat:{\"somecondition\":41}]"), 2254 tuple.MustParse("document:somedoc#banned@user:sarah[testcaveat]"), 2255 }, 2256 ), 2257 2258 obj("document", "somedoc"), 2259 "view", 2260 "user", 2261 2262 "", 2263 []string{"*", "!(sarah needs [somecondition])", "!fred", "!tom"}, 2264 }, 2265 { 2266 "lookup over union", 2267 ` 2268 definition user {} 2269 2270 definition document { 2271 relation editor: user 2272 relation viewer: user 2273 permission view = viewer + editor 2274 } 2275 `, 2276 itestutil.JoinTuples( 2277 itestutil.GenResourceTuples("document", "somedoc", "viewer", "user", "...", 580), 2278 itestutil.GenResourceTuplesWithOffset("document", "somedoc", "editor", "user", "...", 500, 500), 2279 ), 2280 2281 obj("document", "somedoc"), 2282 "view", 2283 "user", 2284 "", 2285 itestutil.GenSubjectIds("user", 1000), 2286 }, 2287 { 2288 "lookup over intersection", 2289 ` 2290 definition user {} 2291 2292 definition document { 2293 relation editor: user 2294 relation viewer: user 2295 permission view = viewer & editor 2296 } 2297 `, 2298 itestutil.JoinTuples( 2299 itestutil.GenResourceTuples("document", "somedoc", "viewer", "user", "...", 580), 2300 itestutil.GenResourceTuplesWithOffset("document", "somedoc", "editor", "user", "...", 500, 500), 2301 ), 2302 2303 obj("document", "somedoc"), 2304 "view", 2305 "user", 2306 "", 2307 itestutil.GenSubjectIdsWithOffset("user", 500, 80), 2308 }, 2309 { 2310 "lookup over exclusion", 2311 ` 2312 definition user {} 2313 2314 definition document { 2315 relation banned: user 2316 relation viewer: user 2317 permission view = viewer - banned 2318 } 2319 `, 2320 itestutil.JoinTuples( 2321 itestutil.GenResourceTuples("document", "somedoc", "viewer", "user", "...", 580), 2322 itestutil.GenResourceTuplesWithOffset("document", "somedoc", "banned", "user", "...", 500, 500), 2323 ), 2324 2325 obj("document", "somedoc"), 2326 "view", 2327 "user", 2328 "", 2329 itestutil.GenSubjectIdsWithOffset("user", 0, 500), 2330 }, 2331 { 2332 "lookup over union with arrow", 2333 ` 2334 definition user {} 2335 2336 definition organization { 2337 relation admin: user 2338 } 2339 2340 definition document { 2341 relation org: organization 2342 relation editor: user 2343 relation viewer: user 2344 permission view = viewer + editor + org->admin 2345 } 2346 `, 2347 itestutil.JoinTuples( 2348 itestutil.GenResourceTuples("document", "somedoc", "viewer", "user", "...", 580), 2349 itestutil.GenResourceTuplesWithOffset("document", "somedoc", "editor", "user", "...", 500, 500), 2350 itestutil.GenResourceTuplesWithOffset("organization", "someorg", "admin", "user", "...", 700, 500), 2351 []*core.RelationTuple{ 2352 tuple.MustParse("document:somedoc#org@organization:someorg"), 2353 }, 2354 ), 2355 2356 obj("document", "somedoc"), 2357 "view", 2358 "user", 2359 "", 2360 itestutil.GenSubjectIds("user", 1200), 2361 }, 2362 { 2363 "lookup over groups", 2364 ` 2365 definition user {} 2366 2367 definition group { 2368 relation direct_member: user | group#member 2369 permission member = direct_member 2370 } 2371 2372 definition document { 2373 relation viewer: user | group#member 2374 permission view = viewer 2375 } 2376 `, 2377 itestutil.JoinTuples( 2378 itestutil.GenResourceTuples("document", "somedoc", "viewer", "user", "...", 580), 2379 itestutil.GenResourceTuplesWithOffset("document", "somedoc", "viewer", "user", "...", 1200, 100), 2380 itestutil.GenResourceTuplesWithOffset("group", "somegroup", "direct_member", "user", "...", 500, 500), 2381 itestutil.GenResourceTuplesWithOffset("group", "childgroup", "direct_member", "user", "...", 700, 500), 2382 []*core.RelationTuple{ 2383 tuple.MustParse("document:somedoc#viewer@group:somegroup#member"), 2384 tuple.MustParse("group:somegroup#direct_member@group:childgroup#member"), 2385 }, 2386 ), 2387 2388 obj("document", "somedoc"), 2389 "view", 2390 "user", 2391 "", 2392 itestutil.GenSubjectIds("user", 1300), 2393 }, 2394 { 2395 "complex schema with disjoint user sets", 2396 ` 2397 definition user {} 2398 2399 definition group { 2400 relation owner: user 2401 relation parent: group 2402 relation direct_member: user | group#member 2403 permission member = owner + direct_member + parent->member 2404 } 2405 2406 definition supercontainer { 2407 relation owner: user | group#member 2408 } 2409 2410 definition container { 2411 relation parent: supercontainer 2412 relation direct_member: user | group#member 2413 relation owner: user | group#member 2414 2415 permission special_ownership = parent->owner 2416 permission member = owner + direct_member 2417 } 2418 2419 definition resource { 2420 relation parent: container 2421 relation viewer: user | group#member 2422 relation owner: user | group#member 2423 2424 permission view = owner + parent->member + viewer + parent->special_ownership 2425 } 2426 `, 2427 itestutil.JoinTuples( 2428 []*core.RelationTuple{ 2429 tuple.MustParse("resource:someresource#owner@user:31#..."), 2430 tuple.MustParse("resource:someresource#parent@container:17#..."), 2431 tuple.MustParse("container:17#direct_member@group:81#member"), 2432 tuple.MustParse("container:17#direct_member@user:11#..."), 2433 tuple.MustParse("container:17#direct_member@user:129#..."), 2434 tuple.MustParse("container:17#direct_member@user:13#..."), 2435 tuple.MustParse("container:17#direct_member@user:130#..."), 2436 tuple.MustParse("container:17#direct_member@user:131#..."), 2437 tuple.MustParse("container:17#direct_member@user:133#..."), 2438 tuple.MustParse("container:17#direct_member@user:134#..."), 2439 tuple.MustParse("container:17#direct_member@user:135#..."), 2440 tuple.MustParse("container:17#direct_member@user:15#..."), 2441 tuple.MustParse("container:17#direct_member@user:16#..."), 2442 tuple.MustParse("container:17#direct_member@user:160#..."), 2443 tuple.MustParse("container:17#direct_member@user:163#..."), 2444 tuple.MustParse("container:17#direct_member@user:166#..."), 2445 tuple.MustParse("container:17#direct_member@user:17#..."), 2446 tuple.MustParse("container:17#direct_member@user:18#..."), 2447 tuple.MustParse("container:17#direct_member@user:19#..."), 2448 tuple.MustParse("container:17#direct_member@user:20#..."), 2449 tuple.MustParse("container:17#direct_member@user:23#..."), 2450 tuple.MustParse("container:17#direct_member@user:244#..."), 2451 tuple.MustParse("container:17#direct_member@user:25#..."), 2452 tuple.MustParse("container:17#direct_member@user:26#..."), 2453 tuple.MustParse("container:17#direct_member@user:262#..."), 2454 tuple.MustParse("container:17#direct_member@user:264#..."), 2455 tuple.MustParse("container:17#direct_member@user:265#..."), 2456 tuple.MustParse("container:17#direct_member@user:267#..."), 2457 tuple.MustParse("container:17#direct_member@user:268#..."), 2458 tuple.MustParse("container:17#direct_member@user:269#..."), 2459 tuple.MustParse("container:17#direct_member@user:27#..."), 2460 tuple.MustParse("container:17#direct_member@user:298#..."), 2461 tuple.MustParse("container:17#direct_member@user:30#..."), 2462 tuple.MustParse("container:17#direct_member@user:31#..."), 2463 tuple.MustParse("container:17#direct_member@user:317#..."), 2464 tuple.MustParse("container:17#direct_member@user:318#..."), 2465 tuple.MustParse("container:17#direct_member@user:32#..."), 2466 tuple.MustParse("container:17#direct_member@user:324#..."), 2467 tuple.MustParse("container:17#direct_member@user:33#..."), 2468 tuple.MustParse("container:17#direct_member@user:34#..."), 2469 tuple.MustParse("container:17#direct_member@user:341#..."), 2470 tuple.MustParse("container:17#direct_member@user:342#..."), 2471 tuple.MustParse("container:17#direct_member@user:343#..."), 2472 tuple.MustParse("container:17#direct_member@user:349#..."), 2473 tuple.MustParse("container:17#direct_member@user:357#..."), 2474 tuple.MustParse("container:17#direct_member@user:361#..."), 2475 tuple.MustParse("container:17#direct_member@user:388#..."), 2476 tuple.MustParse("container:17#direct_member@user:410#..."), 2477 tuple.MustParse("container:17#direct_member@user:430#..."), 2478 tuple.MustParse("container:17#direct_member@user:438#..."), 2479 tuple.MustParse("container:17#direct_member@user:446#..."), 2480 tuple.MustParse("container:17#direct_member@user:448#..."), 2481 tuple.MustParse("container:17#direct_member@user:451#..."), 2482 tuple.MustParse("container:17#direct_member@user:452#..."), 2483 tuple.MustParse("container:17#direct_member@user:453#..."), 2484 tuple.MustParse("container:17#direct_member@user:456#..."), 2485 tuple.MustParse("container:17#direct_member@user:458#..."), 2486 tuple.MustParse("container:17#direct_member@user:459#..."), 2487 tuple.MustParse("container:17#direct_member@user:462#..."), 2488 tuple.MustParse("container:17#direct_member@user:470#..."), 2489 tuple.MustParse("container:17#direct_member@user:471#..."), 2490 tuple.MustParse("container:17#direct_member@user:474#..."), 2491 tuple.MustParse("container:17#direct_member@user:475#..."), 2492 tuple.MustParse("container:17#direct_member@user:476#..."), 2493 tuple.MustParse("container:17#direct_member@user:477#..."), 2494 tuple.MustParse("container:17#direct_member@user:478#..."), 2495 tuple.MustParse("container:17#direct_member@user:480#..."), 2496 tuple.MustParse("container:17#direct_member@user:485#..."), 2497 tuple.MustParse("container:17#direct_member@user:488#..."), 2498 tuple.MustParse("container:17#direct_member@user:490#..."), 2499 tuple.MustParse("container:17#direct_member@user:494#..."), 2500 tuple.MustParse("container:17#direct_member@user:496#..."), 2501 tuple.MustParse("container:17#direct_member@user:506#..."), 2502 tuple.MustParse("container:17#direct_member@user:508#..."), 2503 tuple.MustParse("container:17#direct_member@user:513#..."), 2504 tuple.MustParse("container:17#direct_member@user:514#..."), 2505 tuple.MustParse("container:17#direct_member@user:518#..."), 2506 tuple.MustParse("container:17#direct_member@user:528#..."), 2507 tuple.MustParse("container:17#direct_member@user:530#..."), 2508 tuple.MustParse("container:17#direct_member@user:537#..."), 2509 tuple.MustParse("container:17#direct_member@user:545#..."), 2510 tuple.MustParse("container:17#direct_member@user:614#..."), 2511 tuple.MustParse("container:17#direct_member@user:616#..."), 2512 tuple.MustParse("container:17#direct_member@user:619#..."), 2513 tuple.MustParse("container:17#direct_member@user:620#..."), 2514 tuple.MustParse("container:17#direct_member@user:621#..."), 2515 tuple.MustParse("container:17#direct_member@user:622#..."), 2516 tuple.MustParse("container:17#direct_member@user:624#..."), 2517 tuple.MustParse("container:17#direct_member@user:625#..."), 2518 tuple.MustParse("container:17#direct_member@user:626#..."), 2519 tuple.MustParse("container:17#direct_member@user:629#..."), 2520 tuple.MustParse("container:17#direct_member@user:630#..."), 2521 tuple.MustParse("container:17#direct_member@user:633#..."), 2522 tuple.MustParse("container:17#direct_member@user:635#..."), 2523 tuple.MustParse("container:17#direct_member@user:644#..."), 2524 tuple.MustParse("container:17#direct_member@user:645#..."), 2525 tuple.MustParse("container:17#direct_member@user:646#..."), 2526 tuple.MustParse("container:17#direct_member@user:647#..."), 2527 tuple.MustParse("container:17#direct_member@user:649#..."), 2528 tuple.MustParse("container:17#direct_member@user:652#..."), 2529 tuple.MustParse("container:17#direct_member@user:653#..."), 2530 tuple.MustParse("container:17#direct_member@user:656#..."), 2531 tuple.MustParse("container:17#direct_member@user:657#..."), 2532 tuple.MustParse("container:17#direct_member@user:672#..."), 2533 tuple.MustParse("container:17#direct_member@user:680#..."), 2534 tuple.MustParse("container:17#direct_member@user:687#..."), 2535 tuple.MustParse("container:17#direct_member@user:690#..."), 2536 tuple.MustParse("container:17#direct_member@user:691#..."), 2537 tuple.MustParse("container:17#direct_member@user:698#..."), 2538 tuple.MustParse("container:17#direct_member@user:699#..."), 2539 tuple.MustParse("container:17#direct_member@user:7#..."), 2540 tuple.MustParse("container:17#direct_member@user:700#..."), 2541 tuple.MustParse("container:17#owner@user:3#..."), 2542 tuple.MustParse("container:17#owner@user:378#..."), 2543 tuple.MustParse("container:17#owner@user:410#..."), 2544 tuple.MustParse("container:17#owner@user:651#..."), 2545 tuple.MustParse("container:17#parent@supercontainer:22#..."), 2546 tuple.MustParse("group:81#direct_member@user:11#..."), 2547 tuple.MustParse("group:81#direct_member@user:129#..."), 2548 tuple.MustParse("group:81#direct_member@user:13#..."), 2549 tuple.MustParse("group:81#direct_member@user:130#..."), 2550 tuple.MustParse("group:81#direct_member@user:131#..."), 2551 tuple.MustParse("group:81#direct_member@user:133#..."), 2552 tuple.MustParse("group:81#direct_member@user:134#..."), 2553 tuple.MustParse("group:81#direct_member@user:135#..."), 2554 tuple.MustParse("group:81#direct_member@user:15#..."), 2555 tuple.MustParse("group:81#direct_member@user:156#..."), 2556 tuple.MustParse("group:81#direct_member@user:16#..."), 2557 tuple.MustParse("group:81#direct_member@user:163#..."), 2558 tuple.MustParse("group:81#direct_member@user:166#..."), 2559 tuple.MustParse("group:81#direct_member@user:167#..."), 2560 tuple.MustParse("group:81#direct_member@user:18#..."), 2561 tuple.MustParse("group:81#direct_member@user:19#..."), 2562 tuple.MustParse("group:81#direct_member@user:20#..."), 2563 tuple.MustParse("group:81#direct_member@user:23#..."), 2564 tuple.MustParse("group:81#direct_member@user:24#..."), 2565 tuple.MustParse("group:81#direct_member@user:244#..."), 2566 tuple.MustParse("group:81#direct_member@user:25#..."), 2567 tuple.MustParse("group:81#direct_member@user:26#..."), 2568 tuple.MustParse("group:81#direct_member@user:262#..."), 2569 tuple.MustParse("group:81#direct_member@user:264#..."), 2570 tuple.MustParse("group:81#direct_member@user:265#..."), 2571 tuple.MustParse("group:81#direct_member@user:267#..."), 2572 tuple.MustParse("group:81#direct_member@user:268#..."), 2573 tuple.MustParse("group:81#direct_member@user:269#..."), 2574 tuple.MustParse("group:81#direct_member@user:27#..."), 2575 tuple.MustParse("group:81#direct_member@user:285#..."), 2576 tuple.MustParse("group:81#direct_member@user:286#..."), 2577 tuple.MustParse("group:81#direct_member@user:287#..."), 2578 tuple.MustParse("group:81#direct_member@user:298#..."), 2579 tuple.MustParse("group:81#direct_member@user:30#..."), 2580 tuple.MustParse("group:81#direct_member@user:31#..."), 2581 tuple.MustParse("group:81#direct_member@user:310#..."), 2582 tuple.MustParse("group:81#direct_member@user:317#..."), 2583 tuple.MustParse("group:81#direct_member@user:318#..."), 2584 tuple.MustParse("group:81#direct_member@user:32#..."), 2585 tuple.MustParse("group:81#direct_member@user:324#..."), 2586 tuple.MustParse("group:81#direct_member@user:34#..."), 2587 tuple.MustParse("group:81#direct_member@user:341#..."), 2588 tuple.MustParse("group:81#direct_member@user:342#..."), 2589 tuple.MustParse("group:81#direct_member@user:343#..."), 2590 tuple.MustParse("group:81#direct_member@user:349#..."), 2591 tuple.MustParse("group:81#direct_member@user:371#..."), 2592 tuple.MustParse("group:81#direct_member@user:382#..."), 2593 tuple.MustParse("group:81#direct_member@user:388#..."), 2594 tuple.MustParse("group:81#direct_member@user:4#..."), 2595 tuple.MustParse("group:81#direct_member@user:411#..."), 2596 tuple.MustParse("group:81#direct_member@user:437#..."), 2597 tuple.MustParse("group:81#direct_member@user:438#..."), 2598 tuple.MustParse("group:81#direct_member@user:440#..."), 2599 tuple.MustParse("group:81#direct_member@user:452#..."), 2600 tuple.MustParse("group:81#direct_member@user:481#..."), 2601 tuple.MustParse("group:81#direct_member@user:486#..."), 2602 tuple.MustParse("group:81#direct_member@user:487#..."), 2603 tuple.MustParse("group:81#direct_member@user:529#..."), 2604 tuple.MustParse("group:81#direct_member@user:7#..."), 2605 tuple.MustParse("group:81#parent@group:1#..."), 2606 tuple.MustParse("supercontainer:22#direct_member@user:279#..."), 2607 tuple.MustParse("supercontainer:22#direct_member@user:438#..."), 2608 tuple.MustParse("supercontainer:22#direct_member@user:472#..."), 2609 tuple.MustParse("supercontainer:22#direct_member@user:485#..."), 2610 tuple.MustParse("supercontainer:22#direct_member@user:489#..."), 2611 tuple.MustParse("supercontainer:22#direct_member@user:526#..."), 2612 tuple.MustParse("supercontainer:22#direct_member@user:536#..."), 2613 tuple.MustParse("supercontainer:22#direct_member@user:537#..."), 2614 tuple.MustParse("supercontainer:22#direct_member@user:623#..."), 2615 tuple.MustParse("supercontainer:22#direct_member@user:672#..."), 2616 tuple.MustParse("supercontainer:22#owner@group:3#member"), 2617 tuple.MustParse("supercontainer:22#owner@user:136#..."), 2618 tuple.MustParse("supercontainer:22#owner@user:19#..."), 2619 tuple.MustParse("supercontainer:22#owner@user:21#..."), 2620 tuple.MustParse("supercontainer:22#owner@user:279#..."), 2621 tuple.MustParse("supercontainer:22#owner@user:3#..."), 2622 tuple.MustParse("supercontainer:22#owner@user:31#..."), 2623 tuple.MustParse("supercontainer:22#owner@user:4#..."), 2624 tuple.MustParse("supercontainer:22#owner@user:439#..."), 2625 tuple.MustParse("supercontainer:22#owner@user:500#..."), 2626 tuple.MustParse("supercontainer:22#owner@user:7#..."), 2627 tuple.MustParse("supercontainer:22#owner@user:9#..."), 2628 tuple.MustParse("group:3#direct_member@user:135#..."), 2629 tuple.MustParse("group:3#direct_member@user:160#..."), 2630 tuple.MustParse("group:3#direct_member@user:17#..."), 2631 tuple.MustParse("group:3#direct_member@user:19#..."), 2632 tuple.MustParse("group:3#direct_member@user:272#..."), 2633 tuple.MustParse("group:3#direct_member@user:3#..."), 2634 tuple.MustParse("group:3#direct_member@user:4#..."), 2635 tuple.MustParse("group:3#direct_member@user:439#..."), 2636 tuple.MustParse("group:3#direct_member@user:7#..."), 2637 tuple.MustParse("group:3#direct_member@user:9#..."), 2638 tuple.MustParse("group:1#direct_member@user:12#..."), 2639 tuple.MustParse("group:1#direct_member@user:13#..."), 2640 tuple.MustParse("group:1#direct_member@user:135#..."), 2641 tuple.MustParse("group:1#direct_member@user:14#..."), 2642 tuple.MustParse("group:1#direct_member@user:21#..."), 2643 tuple.MustParse("group:1#direct_member@user:320#..."), 2644 tuple.MustParse("group:1#direct_member@user:321#..."), 2645 tuple.MustParse("group:1#direct_member@user:322#..."), 2646 tuple.MustParse("group:1#direct_member@user:323#..."), 2647 tuple.MustParse("group:1#direct_member@user:34#..."), 2648 tuple.MustParse("group:1#direct_member@user:397#..."), 2649 tuple.MustParse("group:1#direct_member@user:46#..."), 2650 tuple.MustParse("group:1#direct_member@user:50#..."), 2651 tuple.MustParse("group:1#direct_member@user:662#..."), 2652 tuple.MustParse("group:1#owner@user:135#..."), 2653 tuple.MustParse("group:1#owner@user:148#..."), 2654 tuple.MustParse("group:1#owner@user:160#..."), 2655 tuple.MustParse("group:1#owner@user:17#..."), 2656 tuple.MustParse("group:1#owner@user:25#..."), 2657 tuple.MustParse("group:1#owner@user:279#..."), 2658 tuple.MustParse("group:1#owner@user:3#..."), 2659 tuple.MustParse("group:1#owner@user:31#..."), 2660 tuple.MustParse("group:1#owner@user:4#..."), 2661 tuple.MustParse("group:1#owner@user:406#..."), 2662 tuple.MustParse("group:1#owner@user:439#..."), 2663 tuple.MustParse("group:1#owner@user:7#..."), 2664 tuple.MustParse("group:1#owner@user:9#..."), 2665 }, 2666 ), 2667 2668 obj("resource", "someresource"), 2669 "view", 2670 "user", 2671 "", 2672 []string{"11", "12", "129", "13", "130", "131", "133", "134", "135", "136", "14", "148", "15", "156", "16", "160", "163", "166", "167", "17", "18", "19", "20", "21", "23", "24", "244", "25", "26", "262", "264", "265", "267", "268", "269", "27", "272", "279", "285", "286", "287", "298", "3", "30", "31", "310", "317", "318", "32", "320", "321", "322", "323", "324", "33", "34", "341", "342", "343", "349", "357", "361", "371", "378", "382", "388", "397", "4", "406", "410", "411", "430", "437", "438", "439", "440", "446", "448", "451", "452", "453", "456", "458", "459", "46", "462", "470", "471", "474", "475", "476", "477", "478", "480", "481", "485", "486", "487", "488", "490", "494", "496", "50", "500", "506", "508", "513", "514", "518", "528", "529", "530", "537", "545", "614", "616", "619", "620", "621", "622", "624", "625", "626", "629", "630", "633", "635", "644", "645", "646", "647", "649", "651", "652", "653", "656", "657", "662", "672", "680", "687", "690", "691", "698", "699", "7", "700", "9"}, 2673 }, 2674 } 2675 2676 for _, delta := range testTimedeltas { 2677 delta := delta 2678 t.Run(fmt.Sprintf("fuzz%d", delta/time.Millisecond), func(t *testing.T) { 2679 for _, limit := range []int{0, 5, 10, 15, 104, 572} { 2680 limit := limit 2681 t.Run(fmt.Sprintf("limit%d_", limit), func(t *testing.T) { 2682 for _, tc := range testCases { 2683 tc := tc 2684 t.Run(tc.name, func(t *testing.T) { 2685 req := require.New(t) 2686 conn, cleanup, _, revision := testserver.NewTestServer(req, testTimedeltas[0], memdb.DisableGC, true, 2687 func(ds datastore.Datastore, require *require.Assertions) (datastore.Datastore, datastore.Revision) { 2688 return tf.DatastoreFromSchemaAndTestRelationships(ds, tc.schema, tc.relationships, require) 2689 }) 2690 2691 client := v1.NewPermissionsServiceClient(conn) 2692 t.Cleanup(func() { 2693 goleak.VerifyNone(t, goleak.IgnoreCurrent()) 2694 }) 2695 t.Cleanup(cleanup) 2696 2697 var currentCursor *v1.Cursor 2698 foundObjectIds := mapz.NewSet[string]() 2699 2700 for i := 0; i < 500; i++ { 2701 var trailer metadata.MD 2702 2703 lookupClient, err := client.LookupSubjects(context.Background(), &v1.LookupSubjectsRequest{ 2704 Resource: tc.resource, 2705 Permission: tc.permission, 2706 SubjectObjectType: tc.subjectType, 2707 OptionalSubjectRelation: tc.subjectRelation, 2708 Consistency: &v1.Consistency{ 2709 Requirement: &v1.Consistency_AtLeastAsFresh{ 2710 AtLeastAsFresh: zedtoken.MustNewFromRevision(revision), 2711 }, 2712 }, 2713 OptionalConcreteLimit: uint32(limit), 2714 OptionalCursor: currentCursor, 2715 }, grpc.Trailer(&trailer)) 2716 2717 req.NoError(err) 2718 var resolvedObjectIds []string 2719 existingCursor := currentCursor 2720 for { 2721 resp, err := lookupClient.Recv() 2722 if errors.Is(err, io.EOF) { 2723 break 2724 } 2725 2726 req.NoError(err) 2727 2728 subjectID := resp.Subject.SubjectObjectId 2729 if resp.Subject.PartialCaveatInfo != nil { 2730 missingContext := slices.Clone(resp.Subject.PartialCaveatInfo.MissingRequiredContext) 2731 sort.Strings(missingContext) 2732 subjectID = fmt.Sprintf("%v needs [%s]", subjectID, strings.Join(missingContext, ",")) 2733 } 2734 2735 resolvedObjectIds = append(resolvedObjectIds, subjectID) 2736 foundObjectIds.Add(subjectID) 2737 currentCursor = resp.AfterResultCursor 2738 2739 if len(resp.ExcludedSubjects) > 0 { 2740 for _, excludedSubject := range resp.ExcludedSubjects { 2741 if excludedSubject.PartialCaveatInfo == nil { 2742 foundObjectIds.Add(fmt.Sprintf("!%v", excludedSubject.SubjectObjectId)) 2743 } else { 2744 foundObjectIds.Add(fmt.Sprintf("!(%v needs [%s])", excludedSubject.SubjectObjectId, strings.Join(excludedSubject.PartialCaveatInfo.MissingRequiredContext, ","))) 2745 } 2746 } 2747 } 2748 } 2749 2750 if limit > 0 { 2751 allowedLimit := limit 2752 if slices.Contains(tc.expectedSubjectIds, "*") { 2753 allowedLimit++ 2754 } 2755 2756 req.LessOrEqual(len(resolvedObjectIds), allowedLimit, "starting at cursor %v, found: %v", existingCursor, resolvedObjectIds) 2757 } 2758 2759 dispatchCount, err := responsemeta.GetIntResponseTrailerMetadata(trailer, responsemeta.DispatchedOperationsCount) 2760 req.NoError(err) 2761 req.GreaterOrEqual(dispatchCount, 0) 2762 2763 if len(resolvedObjectIds) == 0 || limit == 0 { 2764 break 2765 } 2766 } 2767 2768 allResolvedObjectIds := foundObjectIds.AsSlice() 2769 2770 sort.Strings(tc.expectedSubjectIds) 2771 sort.Strings(allResolvedObjectIds) 2772 2773 req.Equal(tc.expectedSubjectIds, allResolvedObjectIds) 2774 }) 2775 } 2776 }) 2777 } 2778 }) 2779 } 2780 }