github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/experimental_test.go (about) 1 package v1_test 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "math" 9 "math/rand" 10 "strconv" 11 "testing" 12 13 "github.com/authzed/authzed-go/pkg/responsemeta" 14 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 15 "github.com/authzed/grpcutil" 16 "github.com/scylladb/go-set" 17 "github.com/stretchr/testify/require" 18 "go.uber.org/goleak" 19 "google.golang.org/grpc" 20 "google.golang.org/grpc/codes" 21 "google.golang.org/grpc/metadata" 22 "google.golang.org/grpc/status" 23 24 "github.com/authzed/spicedb/internal/datastore/memdb" 25 "github.com/authzed/spicedb/internal/namespace" 26 "github.com/authzed/spicedb/internal/services/shared" 27 tf "github.com/authzed/spicedb/internal/testfixtures" 28 "github.com/authzed/spicedb/internal/testserver" 29 "github.com/authzed/spicedb/pkg/datastore" 30 "github.com/authzed/spicedb/pkg/genutil/mapz" 31 "github.com/authzed/spicedb/pkg/testutil" 32 "github.com/authzed/spicedb/pkg/tuple" 33 ) 34 35 func TestBulkImportRelationships(t *testing.T) { 36 testCases := []struct { 37 name string 38 batchSize func() int 39 numBatches int 40 }{ 41 {"one small batch", constBatch(1), 1}, 42 {"one big batch", constBatch(10_000), 1}, 43 {"many small batches", constBatch(5), 1_000}, 44 {"one empty batch", constBatch(0), 1}, 45 {"small random batches", randomBatch(1, 10), 100}, 46 {"big random batches", randomBatch(1_000, 3_000), 50}, 47 } 48 49 for _, tc := range testCases { 50 t.Run(tc.name, func(t *testing.T) { 51 require := require.New(t) 52 53 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithSchema) 54 client := v1.NewExperimentalServiceClient(conn) 55 t.Cleanup(cleanup) 56 57 ctx := context.Background() 58 59 writer, err := client.BulkImportRelationships(ctx) 60 require.NoError(err) 61 62 var expectedTotal uint64 63 for batchNum := 0; batchNum < tc.numBatches; batchNum++ { 64 batchSize := tc.batchSize() 65 batch := make([]*v1.Relationship, 0, batchSize) 66 67 for i := 0; i < batchSize; i++ { 68 batch = append(batch, rel( 69 tf.DocumentNS.Name, 70 strconv.Itoa(batchNum)+"_"+strconv.Itoa(i), 71 "viewer", 72 tf.UserNS.Name, 73 strconv.Itoa(i), 74 "", 75 )) 76 } 77 78 err := writer.Send(&v1.BulkImportRelationshipsRequest{ 79 Relationships: batch, 80 }) 81 require.NoError(err) 82 83 expectedTotal += uint64(batchSize) 84 } 85 86 resp, err := writer.CloseAndRecv() 87 require.NoError(err) 88 require.Equal(expectedTotal, resp.NumLoaded) 89 90 readerClient := v1.NewPermissionsServiceClient(conn) 91 stream, err := readerClient.ReadRelationships(ctx, &v1.ReadRelationshipsRequest{ 92 RelationshipFilter: &v1.RelationshipFilter{ 93 ResourceType: tf.DocumentNS.Name, 94 }, 95 Consistency: &v1.Consistency{ 96 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 97 }, 98 }) 99 require.NoError(err) 100 101 var readBack uint64 102 for _, err = stream.Recv(); err == nil; _, err = stream.Recv() { 103 readBack++ 104 } 105 require.ErrorIs(err, io.EOF) 106 require.Equal(expectedTotal, readBack) 107 }) 108 } 109 } 110 111 func constBatch(size int) func() int { 112 return func() int { 113 return size 114 } 115 } 116 117 func randomBatch(min, max int) func() int { 118 return func() int { 119 // nolint:gosec 120 // G404 use of non cryptographically secure random number generator is not a security concern here, 121 // as this is only used for generating fixtures in testing. 122 return rand.Intn(max-min) + min 123 } 124 } 125 126 func TestBulkExportRelationshipsBeyondAllowedLimit(t *testing.T) { 127 require := require.New(t) 128 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithData) 129 client := v1.NewExperimentalServiceClient(conn) 130 t.Cleanup(cleanup) 131 132 resp, err := client.BulkExportRelationships(context.Background(), &v1.BulkExportRelationshipsRequest{ 133 OptionalLimit: 10000005, 134 }) 135 require.NoError(err) 136 137 _, err = resp.Recv() 138 require.Error(err) 139 require.Contains(err.Error(), "provided limit 10000005 is greater than maximum allowed of 100000") 140 } 141 142 func TestBulkExportRelationships(t *testing.T) { 143 conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.StandardDatastoreWithSchema) 144 client := v1.NewExperimentalServiceClient(conn) 145 t.Cleanup(cleanup) 146 147 nsAndRels := []struct { 148 namespace string 149 relation string 150 }{ 151 {tf.DocumentNS.Name, "viewer"}, 152 {tf.FolderNS.Name, "viewer"}, 153 {tf.DocumentNS.Name, "owner"}, 154 {tf.FolderNS.Name, "owner"}, 155 {tf.DocumentNS.Name, "editor"}, 156 {tf.FolderNS.Name, "editor"}, 157 } 158 159 totalToWrite := uint64(1_000) 160 expectedRels := set.NewStringSetWithSize(int(totalToWrite)) 161 batch := make([]*v1.Relationship, totalToWrite) 162 for i := range batch { 163 nsAndRel := nsAndRels[i%len(nsAndRels)] 164 rel := rel( 165 nsAndRel.namespace, 166 strconv.Itoa(i), 167 nsAndRel.relation, 168 tf.UserNS.Name, 169 strconv.Itoa(i), 170 "", 171 ) 172 batch[i] = rel 173 expectedRels.Add(tuple.MustStringRelationship(rel)) 174 } 175 176 ctx := context.Background() 177 writer, err := client.BulkImportRelationships(ctx) 178 require.NoError(t, err) 179 180 require.NoError(t, writer.Send(&v1.BulkImportRelationshipsRequest{ 181 Relationships: batch, 182 })) 183 184 resp, err := writer.CloseAndRecv() 185 require.NoError(t, err) 186 require.Equal(t, totalToWrite, resp.NumLoaded) 187 188 testCases := []struct { 189 batchSize uint32 190 paginateEveryN int 191 }{ 192 {1_000, math.MaxInt}, 193 {10, math.MaxInt}, 194 {1_000, 1}, 195 {100, 5}, 196 {97, 7}, 197 } 198 199 for _, tc := range testCases { 200 t.Run(fmt.Sprintf("%d-%d", tc.batchSize, tc.paginateEveryN), func(t *testing.T) { 201 require := require.New(t) 202 203 var totalRead uint64 204 remainingRels := expectedRels.Copy() 205 require.Equal(totalToWrite, uint64(expectedRels.Size())) 206 var cursor *v1.Cursor 207 208 var done bool 209 for !done { 210 streamCtx, cancel := context.WithCancel(ctx) 211 212 stream, err := client.BulkExportRelationships(streamCtx, &v1.BulkExportRelationshipsRequest{ 213 OptionalLimit: tc.batchSize, 214 OptionalCursor: cursor, 215 }) 216 require.NoError(err) 217 218 for i := 0; i < tc.paginateEveryN; i++ { 219 batch, err := stream.Recv() 220 if errors.Is(err, io.EOF) { 221 done = true 222 break 223 } 224 225 require.NoError(err) 226 require.LessOrEqual(uint64(len(batch.Relationships)), uint64(tc.batchSize)) 227 require.NotNil(batch.AfterResultCursor) 228 require.NotEmpty(batch.AfterResultCursor.Token) 229 230 cursor = batch.AfterResultCursor 231 totalRead += uint64(len(batch.Relationships)) 232 233 for _, rel := range batch.Relationships { 234 remainingRels.Remove(tuple.MustStringRelationship(rel)) 235 } 236 } 237 238 cancel() 239 } 240 241 require.Equal(totalToWrite, totalRead) 242 require.True(remainingRels.IsEmpty(), "rels were not exported %#v", remainingRels.List()) 243 }) 244 } 245 } 246 247 func TestBulkExportRelationshipsWithFilter(t *testing.T) { 248 testCases := []struct { 249 name string 250 filter *v1.RelationshipFilter 251 expectedCount int 252 }{ 253 { 254 "basic filter", 255 &v1.RelationshipFilter{ 256 ResourceType: tf.DocumentNS.Name, 257 }, 258 500, 259 }, 260 { 261 "filter by resource ID", 262 &v1.RelationshipFilter{ 263 OptionalResourceId: "12", 264 }, 265 1, 266 }, 267 { 268 "filter by resource ID prefix", 269 &v1.RelationshipFilter{ 270 OptionalResourceIdPrefix: "1", 271 }, 272 111, 273 }, 274 { 275 "filter by resource ID prefix and resource type", 276 &v1.RelationshipFilter{ 277 ResourceType: tf.DocumentNS.Name, 278 OptionalResourceIdPrefix: "1", 279 }, 280 55, 281 }, 282 { 283 "filter by invalid resource type", 284 &v1.RelationshipFilter{ 285 ResourceType: "invalid", 286 }, 287 0, 288 }, 289 } 290 291 batchSize := 14 292 293 for _, tc := range testCases { 294 tc := tc 295 t.Run(tc.name, func(t *testing.T) { 296 require := require.New(t) 297 298 conn, cleanup, _, _ := testserver.NewTestServer(require, 0, memdb.DisableGC, true, tf.StandardDatastoreWithSchema) 299 client := v1.NewExperimentalServiceClient(conn) 300 t.Cleanup(cleanup) 301 302 nsAndRels := []struct { 303 namespace string 304 relation string 305 }{ 306 {tf.DocumentNS.Name, "viewer"}, 307 {tf.FolderNS.Name, "viewer"}, 308 {tf.DocumentNS.Name, "owner"}, 309 {tf.FolderNS.Name, "owner"}, 310 {tf.DocumentNS.Name, "editor"}, 311 {tf.FolderNS.Name, "editor"}, 312 } 313 314 expectedRels := set.NewStringSetWithSize(1000) 315 batch := make([]*v1.Relationship, 1000) 316 for i := range batch { 317 nsAndRel := nsAndRels[i%len(nsAndRels)] 318 rel := rel( 319 nsAndRel.namespace, 320 strconv.Itoa(i), 321 nsAndRel.relation, 322 tf.UserNS.Name, 323 strconv.Itoa(i), 324 "", 325 ) 326 batch[i] = rel 327 328 if tc.filter != nil { 329 filter, err := datastore.RelationshipsFilterFromPublicFilter(tc.filter) 330 require.NoError(err) 331 if !filter.Test(tuple.MustFromRelationship(rel)) { 332 continue 333 } 334 } 335 336 expectedRels.Add(tuple.MustStringRelationship(rel)) 337 } 338 339 require.Equal(tc.expectedCount, expectedRels.Size()) 340 341 ctx := context.Background() 342 writer, err := client.BulkImportRelationships(ctx) 343 require.NoError(err) 344 345 require.NoError(writer.Send(&v1.BulkImportRelationshipsRequest{ 346 Relationships: batch, 347 })) 348 349 _, err = writer.CloseAndRecv() 350 require.NoError(err) 351 352 var totalRead uint64 353 remainingRels := expectedRels.Copy() 354 var cursor *v1.Cursor 355 356 foundRels := mapz.NewSet[string]() 357 for { 358 streamCtx, cancel := context.WithCancel(ctx) 359 360 stream, err := client.BulkExportRelationships(streamCtx, &v1.BulkExportRelationshipsRequest{ 361 OptionalRelationshipFilter: tc.filter, 362 OptionalLimit: uint32(batchSize), 363 OptionalCursor: cursor, 364 }) 365 require.NoError(err) 366 367 batch, err := stream.Recv() 368 if errors.Is(err, io.EOF) { 369 cancel() 370 break 371 } 372 373 require.NoError(err) 374 require.LessOrEqual(uint32(len(batch.Relationships)), uint32(batchSize)) 375 require.NotNil(batch.AfterResultCursor) 376 require.NotEmpty(batch.AfterResultCursor.Token) 377 378 cursor = batch.AfterResultCursor 379 totalRead += uint64(len(batch.Relationships)) 380 381 for _, rel := range batch.Relationships { 382 if tc.filter != nil { 383 filter, err := datastore.RelationshipsFilterFromPublicFilter(tc.filter) 384 require.NoError(err) 385 require.True(filter.Test(tuple.MustFromRelationship(rel)), "relationship did not match filter: %s", rel) 386 } 387 388 require.True(remainingRels.Has(tuple.MustStringRelationship(rel)), "relationship was not expected or was repeated: %s", rel) 389 remainingRels.Remove(tuple.MustStringRelationship(rel)) 390 foundRels.Add(tuple.MustStringRelationship(rel)) 391 } 392 393 cancel() 394 } 395 396 require.Equal(uint64(tc.expectedCount), totalRead, "found: %v", foundRels.AsSlice()) 397 require.True(remainingRels.IsEmpty(), "rels were not exported %#v", remainingRels.List()) 398 }) 399 } 400 } 401 402 type bulkCheckTest struct { 403 req string 404 resp v1.CheckPermissionResponse_Permissionship 405 partial []string 406 err error 407 } 408 409 func TestBulkCheckPermission(t *testing.T) { 410 defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) 411 412 conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.StandardDatastoreWithCaveatedData) 413 client := v1.NewExperimentalServiceClient(conn) 414 defer cleanup() 415 416 testCases := []struct { 417 name string 418 requests []string 419 response []bulkCheckTest 420 expectedDispatchCount int 421 }{ 422 { 423 name: "same resource and permission, different subjects", 424 requests: []string{ 425 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 426 `document:masterplan#view@user:product_manager[test:{"secret": "1234"}]`, 427 `document:masterplan#view@user:villain[test:{"secret": "1234"}]`, 428 }, 429 response: []bulkCheckTest{ 430 { 431 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 432 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 433 }, 434 { 435 req: `document:masterplan#view@user:product_manager[test:{"secret": "1234"}]`, 436 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 437 }, 438 { 439 req: `document:masterplan#view@user:villain[test:{"secret": "1234"}]`, 440 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 441 }, 442 }, 443 expectedDispatchCount: 49, 444 }, 445 { 446 name: "different resources, same permission and subject", 447 requests: []string{ 448 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 449 `document:companyplan#view@user:eng_lead[test:{"secret": "1234"}]`, 450 `document:healthplan#view@user:eng_lead[test:{"secret": "1234"}]`, 451 }, 452 response: []bulkCheckTest{ 453 { 454 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 455 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 456 }, 457 { 458 req: `document:companyplan#view@user:eng_lead[test:{"secret": "1234"}]`, 459 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 460 }, 461 { 462 req: `document:healthplan#view@user:eng_lead[test:{"secret": "1234"}]`, 463 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 464 }, 465 }, 466 expectedDispatchCount: 18, 467 }, 468 { 469 name: "some items fail", 470 requests: []string{ 471 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 472 "fake:fake#fake@fake:fake", 473 "superfake:plan#view@user:eng_lead", 474 }, 475 response: []bulkCheckTest{ 476 { 477 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 478 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 479 }, 480 { 481 req: "fake:fake#fake@fake:fake", 482 err: namespace.NewNamespaceNotFoundErr("fake"), 483 }, 484 { 485 req: "superfake:plan#view@user:eng_lead", 486 err: namespace.NewNamespaceNotFoundErr("superfake"), 487 }, 488 }, 489 expectedDispatchCount: 17, 490 }, 491 { 492 name: "different caveat context is not clustered", 493 requests: []string{ 494 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 495 `document:companyplan#view@user:eng_lead[test:{"secret": "1234"}]`, 496 `document:masterplan#view@user:eng_lead[test:{"secret": "4321"}]`, 497 `document:masterplan#view@user:eng_lead`, 498 }, 499 response: []bulkCheckTest{ 500 { 501 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 502 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 503 }, 504 { 505 req: `document:companyplan#view@user:eng_lead[test:{"secret": "1234"}]`, 506 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 507 }, 508 { 509 req: `document:masterplan#view@user:eng_lead[test:{"secret": "4321"}]`, 510 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 511 }, 512 { 513 req: `document:masterplan#view@user:eng_lead`, 514 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION, 515 partial: []string{"secret"}, 516 }, 517 }, 518 expectedDispatchCount: 50, 519 }, 520 { 521 name: "namespace validation", 522 requests: []string{ 523 "document:masterplan#view@fake:fake", 524 "fake:fake#fake@user:eng_lead", 525 }, 526 response: []bulkCheckTest{ 527 { 528 req: "document:masterplan#view@fake:fake", 529 err: namespace.NewNamespaceNotFoundErr("fake"), 530 }, 531 { 532 req: "fake:fake#fake@user:eng_lead", 533 err: namespace.NewNamespaceNotFoundErr("fake"), 534 }, 535 }, 536 expectedDispatchCount: 1, 537 }, 538 { 539 name: "chunking test", 540 requests: (func() []string { 541 toReturn := make([]string, 0, datastore.FilterMaximumIDCount+5) 542 for i := 0; i < int(datastore.FilterMaximumIDCount+5); i++ { 543 toReturn = append(toReturn, fmt.Sprintf(`document:masterplan-%d#view@user:eng_lead`, i)) 544 } 545 546 return toReturn 547 })(), 548 response: (func() []bulkCheckTest { 549 toReturn := make([]bulkCheckTest, 0, datastore.FilterMaximumIDCount+5) 550 for i := 0; i < int(datastore.FilterMaximumIDCount+5); i++ { 551 toReturn = append(toReturn, bulkCheckTest{ 552 req: fmt.Sprintf(`document:masterplan-%d#view@user:eng_lead`, i), 553 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 554 }) 555 } 556 557 return toReturn 558 })(), 559 expectedDispatchCount: 11, 560 }, 561 { 562 name: "chunking test with errors", 563 requests: (func() []string { 564 toReturn := make([]string, 0, datastore.FilterMaximumIDCount+6) 565 toReturn = append(toReturn, `nondoc:masterplan#view@user:eng_lead`) 566 567 for i := 0; i < int(datastore.FilterMaximumIDCount+5); i++ { 568 toReturn = append(toReturn, fmt.Sprintf(`document:masterplan-%d#view@user:eng_lead`, i)) 569 } 570 571 return toReturn 572 })(), 573 response: (func() []bulkCheckTest { 574 toReturn := make([]bulkCheckTest, 0, datastore.FilterMaximumIDCount+6) 575 toReturn = append(toReturn, bulkCheckTest{ 576 req: `nondoc:masterplan#view@user:eng_lead`, 577 err: namespace.NewNamespaceNotFoundErr("nondoc"), 578 }) 579 580 for i := 0; i < int(datastore.FilterMaximumIDCount+5); i++ { 581 toReturn = append(toReturn, bulkCheckTest{ 582 req: fmt.Sprintf(`document:masterplan-%d#view@user:eng_lead`, i), 583 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, 584 }) 585 } 586 587 return toReturn 588 })(), 589 expectedDispatchCount: 11, 590 }, 591 { 592 name: "same resource and permission with same subject, repeated", 593 requests: []string{ 594 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 595 `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 596 }, 597 response: []bulkCheckTest{ 598 { 599 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 600 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 601 }, 602 { 603 req: `document:masterplan#view@user:eng_lead[test:{"secret": "1234"}]`, 604 resp: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, 605 }, 606 }, 607 expectedDispatchCount: 17, 608 }, 609 } 610 611 for _, tt := range testCases { 612 tt := tt 613 t.Run(tt.name, func(t *testing.T) { 614 req := v1.BulkCheckPermissionRequest{ 615 Consistency: &v1.Consistency{ 616 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 617 }, 618 Items: make([]*v1.BulkCheckPermissionRequestItem, 0, len(tt.requests)), 619 } 620 621 for _, r := range tt.requests { 622 req.Items = append(req.Items, relToBulkRequestItem(r)) 623 } 624 625 expected := make([]*v1.BulkCheckPermissionPair, 0, len(tt.response)) 626 for _, r := range tt.response { 627 reqRel := tuple.ParseRel(r.req) 628 resp := &v1.BulkCheckPermissionPair_Item{ 629 Item: &v1.BulkCheckPermissionResponseItem{ 630 Permissionship: r.resp, 631 }, 632 } 633 pair := &v1.BulkCheckPermissionPair{ 634 Request: &v1.BulkCheckPermissionRequestItem{ 635 Resource: reqRel.Resource, 636 Permission: reqRel.Relation, 637 Subject: reqRel.Subject, 638 }, 639 Response: resp, 640 } 641 if reqRel.OptionalCaveat != nil { 642 pair.Request.Context = reqRel.OptionalCaveat.Context 643 } 644 if len(r.partial) > 0 { 645 resp.Item.PartialCaveatInfo = &v1.PartialCaveatInfo{ 646 MissingRequiredContext: r.partial, 647 } 648 } 649 650 if r.err != nil { 651 rewritten := shared.RewriteError(context.Background(), r.err, &shared.ConfigForErrors{}) 652 s, ok := status.FromError(rewritten) 653 require.True(t, ok, "expected provided error to be status") 654 pair.Response = &v1.BulkCheckPermissionPair_Error{ 655 Error: s.Proto(), 656 } 657 } 658 expected = append(expected, pair) 659 } 660 661 var trailer metadata.MD 662 actual, err := client.BulkCheckPermission(context.Background(), &req, grpc.Trailer(&trailer)) 663 require.NoError(t, err) 664 665 dispatchCount, err := responsemeta.GetIntResponseTrailerMetadata(trailer, responsemeta.DispatchedOperationsCount) 666 require.NoError(t, err) 667 require.Equal(t, tt.expectedDispatchCount, dispatchCount) 668 669 testutil.RequireProtoSlicesEqual(t, expected, actual.Pairs, nil, "response bulk check pairs did not match") 670 }) 671 } 672 } 673 674 func relToBulkRequestItem(rel string) *v1.BulkCheckPermissionRequestItem { 675 r := tuple.ParseRel(rel) 676 item := &v1.BulkCheckPermissionRequestItem{ 677 Resource: r.Resource, 678 Permission: r.Relation, 679 Subject: r.Subject, 680 } 681 if r.OptionalCaveat != nil { 682 item.Context = r.OptionalCaveat.Context 683 } 684 return item 685 } 686 687 func TestExperimentalSchemaDiff(t *testing.T) { 688 conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore) 689 expClient := v1.NewExperimentalServiceClient(conn) 690 schemaClient := v1.NewSchemaServiceClient(conn) 691 defer cleanup() 692 693 testCases := []struct { 694 name string 695 existingSchema string 696 comparisonSchema string 697 expectedError string 698 expectedCode codes.Code 699 expectedResponse *v1.ExperimentalDiffSchemaResponse 700 }{ 701 { 702 name: "no changes", 703 existingSchema: `definition user {}`, 704 comparisonSchema: `definition user {}`, 705 expectedResponse: &v1.ExperimentalDiffSchemaResponse{}, 706 }, 707 { 708 name: "addition from existing schema", 709 existingSchema: `definition user {}`, 710 comparisonSchema: `definition user {} definition document {}`, 711 expectedResponse: &v1.ExperimentalDiffSchemaResponse{ 712 Diffs: []*v1.ExpSchemaDiff{ 713 { 714 Diff: &v1.ExpSchemaDiff_DefinitionAdded{ 715 DefinitionAdded: &v1.ExpDefinition{ 716 Name: "document", 717 Comment: "", 718 }, 719 }, 720 }, 721 }, 722 }, 723 }, 724 { 725 name: "removal from existing schema", 726 existingSchema: `definition user {} definition document {}`, 727 comparisonSchema: `definition user {}`, 728 expectedResponse: &v1.ExperimentalDiffSchemaResponse{ 729 Diffs: []*v1.ExpSchemaDiff{ 730 { 731 Diff: &v1.ExpSchemaDiff_DefinitionRemoved{ 732 DefinitionRemoved: &v1.ExpDefinition{ 733 Name: "document", 734 Comment: "", 735 }, 736 }, 737 }, 738 }, 739 }, 740 }, 741 { 742 name: "invalid comparison schema", 743 existingSchema: `definition user {}`, 744 comparisonSchema: `definition user { invalid`, 745 expectedCode: codes.InvalidArgument, 746 expectedError: "Expected end of statement or definition, found: TokenTypeIdentifier", 747 }, 748 } 749 750 for _, tt := range testCases { 751 tt := tt 752 t.Run(tt.name, func(t *testing.T) { 753 // Write the existing schema. 754 _, err := schemaClient.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ 755 Schema: tt.existingSchema, 756 }) 757 require.NoError(t, err) 758 759 actual, err := expClient.ExperimentalDiffSchema(context.Background(), &v1.ExperimentalDiffSchemaRequest{ 760 ComparisonSchema: tt.comparisonSchema, 761 Consistency: &v1.Consistency{ 762 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 763 }, 764 }) 765 766 if tt.expectedError != "" { 767 require.Error(t, err) 768 require.Contains(t, err.Error(), tt.expectedError) 769 grpcutil.RequireStatus(t, tt.expectedCode, err) 770 } else { 771 require.NoError(t, err) 772 require.NotNil(t, actual.ReadAt) 773 actual.ReadAt = nil 774 775 testutil.RequireProtoEqual(t, tt.expectedResponse, actual, "mismatch in response") 776 } 777 }) 778 } 779 } 780 781 func TestExperimentalReflectSchema(t *testing.T) { 782 conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore) 783 expClient := v1.NewExperimentalServiceClient(conn) 784 schemaClient := v1.NewSchemaServiceClient(conn) 785 defer cleanup() 786 787 testCases := []struct { 788 name string 789 schema string 790 filters []*v1.ExpSchemaFilter 791 expectedCode codes.Code 792 expectedError string 793 expectedResponse *v1.ExperimentalReflectSchemaResponse 794 }{ 795 { 796 name: "simple schema", 797 schema: `definition user {}`, 798 expectedResponse: &v1.ExperimentalReflectSchemaResponse{ 799 Definitions: []*v1.ExpDefinition{ 800 { 801 Name: "user", 802 Comment: "", 803 }, 804 }, 805 }, 806 }, 807 { 808 name: "schema with comment", 809 schema: `// this is a user 810 definition user {}`, 811 expectedResponse: &v1.ExperimentalReflectSchemaResponse{ 812 Definitions: []*v1.ExpDefinition{ 813 { 814 Name: "user", 815 Comment: "// this is a user", 816 }, 817 }, 818 }, 819 }, 820 { 821 name: "invalid filter", 822 schema: `definition user {}`, 823 filters: []*v1.ExpSchemaFilter{ 824 { 825 OptionalDefinitionNameFilter: "doc", 826 OptionalCaveatNameFilter: "invalid", 827 }, 828 }, 829 expectedCode: codes.InvalidArgument, 830 expectedError: "cannot filter by both definition and caveat name", 831 }, 832 { 833 name: "another invalid filter", 834 schema: `definition user {}`, 835 filters: []*v1.ExpSchemaFilter{ 836 { 837 OptionalRelationNameFilter: "doc", 838 }, 839 }, 840 expectedCode: codes.InvalidArgument, 841 expectedError: "relation name match requires definition name match", 842 }, 843 { 844 name: "full schema", 845 schema: ` 846 /** user represents a user */ 847 definition user {} 848 849 /** group represents a group */ 850 definition group { 851 relation direct_member: user | group#member 852 relation admin: user 853 permission member = direct_member + admin 854 } 855 856 /** somecaveat is a caveat */ 857 caveat somecaveat(first int, second string) { 858 first == 1 && second == "two" 859 } 860 861 /** document is a protected document */ 862 definition document { 863 // editor is a relation 864 relation editor: user | group#member 865 relation viewer: user | user with somecaveat | group#member | user:* 866 867 // read all the things 868 permission read = viewer + editor 869 } 870 `, 871 expectedResponse: &v1.ExperimentalReflectSchemaResponse{ 872 Definitions: []*v1.ExpDefinition{ 873 { 874 Name: "document", 875 Comment: "/** document is a protected document */", 876 Relations: []*v1.ExpRelation{ 877 { 878 Name: "editor", 879 Comment: "// editor is a relation", 880 ParentDefinitionName: "document", 881 SubjectTypes: []*v1.ExpTypeReference{ 882 { 883 SubjectDefinitionName: "user", 884 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 885 }, 886 { 887 SubjectDefinitionName: "group", 888 Typeref: &v1.ExpTypeReference_OptionalRelationName{ 889 OptionalRelationName: "member", 890 }, 891 }, 892 }, 893 }, 894 { 895 Name: "viewer", 896 Comment: "", 897 ParentDefinitionName: "document", 898 SubjectTypes: []*v1.ExpTypeReference{ 899 { 900 SubjectDefinitionName: "user", 901 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 902 }, 903 { 904 SubjectDefinitionName: "user", 905 OptionalCaveatName: "somecaveat", 906 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 907 }, 908 { 909 SubjectDefinitionName: "group", 910 Typeref: &v1.ExpTypeReference_OptionalRelationName{ 911 OptionalRelationName: "member", 912 }, 913 }, 914 { 915 SubjectDefinitionName: "user", 916 Typeref: &v1.ExpTypeReference_IsPublicWildcard{ 917 IsPublicWildcard: true, 918 }, 919 }, 920 }, 921 }, 922 }, 923 Permissions: []*v1.ExpPermission{ 924 { 925 Name: "read", 926 Comment: "// read all the things", 927 ParentDefinitionName: "document", 928 }, 929 }, 930 }, 931 { 932 Name: "group", 933 Comment: "/** group represents a group */", 934 Relations: []*v1.ExpRelation{ 935 { 936 Name: "direct_member", 937 Comment: "", 938 ParentDefinitionName: "group", 939 SubjectTypes: []*v1.ExpTypeReference{ 940 { 941 SubjectDefinitionName: "user", 942 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 943 }, 944 { 945 SubjectDefinitionName: "group", 946 Typeref: &v1.ExpTypeReference_OptionalRelationName{OptionalRelationName: "member"}, 947 }, 948 }, 949 }, 950 { 951 Name: "admin", 952 Comment: "", 953 ParentDefinitionName: "group", 954 SubjectTypes: []*v1.ExpTypeReference{ 955 { 956 SubjectDefinitionName: "user", 957 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 958 }, 959 }, 960 }, 961 }, 962 Permissions: []*v1.ExpPermission{ 963 { 964 Name: "member", 965 Comment: "", 966 ParentDefinitionName: "group", 967 }, 968 }, 969 }, 970 { 971 Name: "user", 972 Comment: "/** user represents a user */", 973 }, 974 }, 975 Caveats: []*v1.ExpCaveat{ 976 { 977 Name: "somecaveat", 978 Comment: "/** somecaveat is a caveat */", 979 Expression: "first == 1 && second == \"two\"", 980 Parameters: []*v1.ExpCaveatParameter{ 981 { 982 Name: "first", 983 Type: "int", 984 ParentCaveatName: "somecaveat", 985 }, 986 { 987 Name: "second", 988 Type: "string", 989 ParentCaveatName: "somecaveat", 990 }, 991 }, 992 }, 993 }, 994 }, 995 }, 996 { 997 name: "full schema with definition filter", 998 schema: ` 999 /** user represents a user */ 1000 definition user {} 1001 1002 /** group represents a group */ 1003 definition group { 1004 relation direct_member: user | group#member 1005 relation admin: user 1006 permission member = direct_member + admin 1007 } 1008 1009 caveat somecaveat(first int, second string) { 1010 first == 1 && second == "two" 1011 } 1012 1013 /** document is a protected document */ 1014 definition document { 1015 // editor is a relation 1016 relation editor: user | group#member 1017 relation viewer: user | user with somecaveat | group#member 1018 1019 // read all the things 1020 permission read = viewer + editor 1021 } 1022 `, 1023 filters: []*v1.ExpSchemaFilter{ 1024 { 1025 OptionalDefinitionNameFilter: "doc", 1026 }, 1027 }, 1028 expectedResponse: &v1.ExperimentalReflectSchemaResponse{ 1029 Definitions: []*v1.ExpDefinition{ 1030 { 1031 Name: "document", 1032 Comment: "/** document is a protected document */", 1033 Relations: []*v1.ExpRelation{ 1034 { 1035 Name: "editor", 1036 Comment: "// editor is a relation", 1037 ParentDefinitionName: "document", 1038 SubjectTypes: []*v1.ExpTypeReference{ 1039 { 1040 SubjectDefinitionName: "user", 1041 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 1042 }, 1043 { 1044 SubjectDefinitionName: "group", 1045 Typeref: &v1.ExpTypeReference_OptionalRelationName{ 1046 OptionalRelationName: "member", 1047 }, 1048 }, 1049 }, 1050 }, 1051 { 1052 Name: "viewer", 1053 Comment: "", 1054 ParentDefinitionName: "document", 1055 SubjectTypes: []*v1.ExpTypeReference{ 1056 { 1057 SubjectDefinitionName: "user", 1058 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 1059 }, 1060 { 1061 SubjectDefinitionName: "user", 1062 OptionalCaveatName: "somecaveat", 1063 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 1064 }, 1065 { 1066 SubjectDefinitionName: "group", 1067 Typeref: &v1.ExpTypeReference_OptionalRelationName{ 1068 OptionalRelationName: "member", 1069 }, 1070 }, 1071 }, 1072 }, 1073 }, 1074 Permissions: []*v1.ExpPermission{ 1075 { 1076 Name: "read", 1077 Comment: "// read all the things", 1078 ParentDefinitionName: "document", 1079 }, 1080 }, 1081 }, 1082 }, 1083 }, 1084 }, 1085 { 1086 name: "full schema with definition, relation and permission filters", 1087 schema: ` 1088 /** user represents a user */ 1089 definition user {} 1090 1091 /** group represents a group */ 1092 definition group { 1093 relation direct_member: user | group#member 1094 relation admin: user 1095 permission member = direct_member + admin 1096 } 1097 1098 caveat somecaveat(first int, second string) { 1099 first == 1 && second == "two" 1100 } 1101 1102 /** document is a protected document */ 1103 definition document { 1104 // editor is a relation 1105 relation editor: user | group#member 1106 relation viewer: user | user with somecaveat | group#member 1107 1108 // read all the things 1109 permission read = viewer + editor 1110 } 1111 `, 1112 filters: []*v1.ExpSchemaFilter{ 1113 { 1114 OptionalDefinitionNameFilter: "doc", 1115 OptionalRelationNameFilter: "viewer", 1116 }, 1117 { 1118 OptionalDefinitionNameFilter: "doc", 1119 OptionalPermissionNameFilter: "read", 1120 }, 1121 }, 1122 expectedResponse: &v1.ExperimentalReflectSchemaResponse{ 1123 Definitions: []*v1.ExpDefinition{ 1124 { 1125 Name: "document", 1126 Comment: "/** document is a protected document */", 1127 Relations: []*v1.ExpRelation{ 1128 { 1129 Name: "viewer", 1130 Comment: "", 1131 ParentDefinitionName: "document", 1132 SubjectTypes: []*v1.ExpTypeReference{ 1133 { 1134 SubjectDefinitionName: "user", 1135 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 1136 }, 1137 { 1138 SubjectDefinitionName: "user", 1139 OptionalCaveatName: "somecaveat", 1140 Typeref: &v1.ExpTypeReference_IsTerminalSubject{}, 1141 }, 1142 { 1143 SubjectDefinitionName: "group", 1144 Typeref: &v1.ExpTypeReference_OptionalRelationName{ 1145 OptionalRelationName: "member", 1146 }, 1147 }, 1148 }, 1149 }, 1150 }, 1151 Permissions: []*v1.ExpPermission{ 1152 { 1153 Name: "read", 1154 Comment: "// read all the things", 1155 ParentDefinitionName: "document", 1156 }, 1157 }, 1158 }, 1159 }, 1160 }, 1161 }, 1162 } 1163 1164 for _, tt := range testCases { 1165 tt := tt 1166 t.Run(tt.name, func(t *testing.T) { 1167 // Write the schema. 1168 _, err := schemaClient.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ 1169 Schema: tt.schema, 1170 }) 1171 require.NoError(t, err) 1172 1173 actual, err := expClient.ExperimentalReflectSchema(context.Background(), &v1.ExperimentalReflectSchemaRequest{ 1174 OptionalFilters: tt.filters, 1175 Consistency: &v1.Consistency{ 1176 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 1177 }, 1178 }) 1179 1180 if tt.expectedError != "" { 1181 require.Error(t, err) 1182 require.Contains(t, err.Error(), tt.expectedError) 1183 grpcutil.RequireStatus(t, tt.expectedCode, err) 1184 } else { 1185 require.NoError(t, err) 1186 require.NotNil(t, actual.ReadAt) 1187 actual.ReadAt = nil 1188 1189 testutil.RequireProtoEqual(t, tt.expectedResponse, actual, "mismatch in response") 1190 } 1191 }) 1192 } 1193 } 1194 1195 func TestExperimentalDependentRelations(t *testing.T) { 1196 tcs := []struct { 1197 name string 1198 schema string 1199 definitionName string 1200 permissionName string 1201 expectedCode codes.Code 1202 expectedError string 1203 expectedResponse []*v1.ExpRelationReference 1204 }{ 1205 { 1206 name: "invalid definition", 1207 schema: `definition user {}`, 1208 definitionName: "invalid", 1209 expectedCode: codes.FailedPrecondition, 1210 expectedError: "object definition `invalid` not found", 1211 }, 1212 { 1213 name: "invalid permission", 1214 schema: `definition user {}`, 1215 definitionName: "user", 1216 permissionName: "invalid", 1217 expectedCode: codes.FailedPrecondition, 1218 expectedError: "permission `invalid` not found", 1219 }, 1220 { 1221 name: "specified relation", 1222 schema: ` 1223 definition user {} 1224 1225 definition document { 1226 relation editor: user 1227 } 1228 `, 1229 definitionName: "document", 1230 permissionName: "editor", 1231 expectedCode: codes.InvalidArgument, 1232 expectedError: "is not a permission", 1233 }, 1234 { 1235 name: "simple schema", 1236 schema: ` 1237 definition user {} 1238 1239 definition document { 1240 relation unused: user 1241 relation editor: user 1242 relation viewer: user 1243 permission view = viewer + editor 1244 } 1245 `, 1246 definitionName: "document", 1247 permissionName: "view", 1248 expectedResponse: []*v1.ExpRelationReference{ 1249 { 1250 DefinitionName: "document", 1251 RelationName: "editor", 1252 IsPermission: false, 1253 }, 1254 { 1255 DefinitionName: "document", 1256 RelationName: "viewer", 1257 IsPermission: false, 1258 }, 1259 }, 1260 }, 1261 { 1262 name: "schema with nested relation", 1263 schema: ` 1264 definition user {} 1265 1266 definition group { 1267 relation direct_member: user | group#member 1268 relation admin: user 1269 permission member = direct_member + admin 1270 } 1271 1272 definition document { 1273 relation unused: user 1274 relation viewer: user | group#member 1275 permission view = viewer 1276 } 1277 `, 1278 definitionName: "document", 1279 permissionName: "view", 1280 expectedResponse: []*v1.ExpRelationReference{ 1281 { 1282 DefinitionName: "document", 1283 RelationName: "viewer", 1284 IsPermission: false, 1285 }, 1286 { 1287 DefinitionName: "group", 1288 RelationName: "admin", 1289 IsPermission: false, 1290 }, 1291 { 1292 DefinitionName: "group", 1293 RelationName: "direct_member", 1294 IsPermission: false, 1295 }, 1296 { 1297 DefinitionName: "group", 1298 RelationName: "member", 1299 IsPermission: true, 1300 }, 1301 }, 1302 }, 1303 { 1304 name: "schema with arrow", 1305 schema: ` 1306 definition user {} 1307 1308 definition folder { 1309 relation alsounused: user 1310 relation viewer: user 1311 permission view = viewer 1312 } 1313 1314 definition document { 1315 relation unused: user 1316 relation parent: folder 1317 relation viewer: user 1318 permission view = viewer + parent->view 1319 } 1320 `, 1321 definitionName: "document", 1322 permissionName: "view", 1323 expectedResponse: []*v1.ExpRelationReference{ 1324 { 1325 DefinitionName: "document", 1326 RelationName: "parent", 1327 IsPermission: false, 1328 }, 1329 { 1330 DefinitionName: "document", 1331 RelationName: "viewer", 1332 IsPermission: false, 1333 }, 1334 { 1335 DefinitionName: "folder", 1336 RelationName: "view", 1337 IsPermission: true, 1338 }, 1339 { 1340 DefinitionName: "folder", 1341 RelationName: "viewer", 1342 IsPermission: false, 1343 }, 1344 }, 1345 }, 1346 { 1347 name: "empty response", 1348 schema: ` 1349 definition user {} 1350 1351 definition folder { 1352 relation alsounused: user 1353 relation viewer: user 1354 permission view = viewer 1355 } 1356 1357 definition document { 1358 relation unused: user 1359 relation parent: folder 1360 relation viewer: user 1361 permission view = viewer + parent->view 1362 permission empty = nil 1363 } 1364 `, 1365 definitionName: "document", 1366 permissionName: "empty", 1367 expectedResponse: []*v1.ExpRelationReference{}, 1368 }, 1369 { 1370 name: "empty definition", 1371 schema: ` 1372 definition user {} 1373 `, 1374 definitionName: "", 1375 permissionName: "empty", 1376 expectedCode: codes.FailedPrecondition, 1377 expectedError: "object definition `` not found", 1378 }, 1379 { 1380 name: "empty permission", 1381 schema: ` 1382 definition user {} 1383 `, 1384 definitionName: "user", 1385 permissionName: "", 1386 expectedCode: codes.FailedPrecondition, 1387 expectedError: "permission `` not found", 1388 }, 1389 } 1390 1391 for _, tc := range tcs { 1392 tc := tc 1393 t.Run(tc.name, func(t *testing.T) { 1394 conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore) 1395 expClient := v1.NewExperimentalServiceClient(conn) 1396 schemaClient := v1.NewSchemaServiceClient(conn) 1397 defer cleanup() 1398 1399 // Write the schema. 1400 _, err := schemaClient.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ 1401 Schema: tc.schema, 1402 }) 1403 require.NoError(t, err) 1404 1405 actual, err := expClient.ExperimentalDependentRelations(context.Background(), &v1.ExperimentalDependentRelationsRequest{ 1406 DefinitionName: tc.definitionName, 1407 PermissionName: tc.permissionName, 1408 Consistency: &v1.Consistency{ 1409 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 1410 }, 1411 }) 1412 1413 if tc.expectedError != "" { 1414 require.Error(t, err) 1415 require.Contains(t, err.Error(), tc.expectedError) 1416 grpcutil.RequireStatus(t, tc.expectedCode, err) 1417 } else { 1418 require.NoError(t, err) 1419 require.NotNil(t, actual.ReadAt) 1420 actual.ReadAt = nil 1421 1422 testutil.RequireProtoEqual(t, &v1.ExperimentalDependentRelationsResponse{ 1423 Relations: tc.expectedResponse, 1424 }, actual, "mismatch in response") 1425 } 1426 }) 1427 } 1428 } 1429 1430 func TestExperimentalComputablePermissions(t *testing.T) { 1431 tcs := []struct { 1432 name string 1433 schema string 1434 definitionName string 1435 relationName string 1436 filter string 1437 expectedCode codes.Code 1438 expectedError string 1439 expectedResponse []*v1.ExpRelationReference 1440 }{ 1441 { 1442 name: "invalid definition", 1443 schema: `definition user {}`, 1444 definitionName: "invalid", 1445 expectedCode: codes.FailedPrecondition, 1446 expectedError: "object definition `invalid` not found", 1447 }, 1448 { 1449 name: "invalid relation", 1450 schema: `definition user {}`, 1451 definitionName: "user", 1452 relationName: "invalid", 1453 expectedCode: codes.FailedPrecondition, 1454 expectedError: "relation/permission `invalid` not found", 1455 }, 1456 { 1457 name: "basic", 1458 schema: ` 1459 definition user {} 1460 1461 definition document { 1462 relation unused: user 1463 relation editor: user 1464 relation viewer: user 1465 permission view = viewer + editor 1466 permission another = unused 1467 }`, 1468 definitionName: "user", 1469 relationName: "", 1470 expectedResponse: []*v1.ExpRelationReference{ 1471 { 1472 DefinitionName: "document", 1473 RelationName: "another", 1474 IsPermission: true, 1475 }, 1476 { 1477 DefinitionName: "document", 1478 RelationName: "editor", 1479 IsPermission: false, 1480 }, 1481 { 1482 DefinitionName: "document", 1483 RelationName: "unused", 1484 IsPermission: false, 1485 }, 1486 { 1487 DefinitionName: "document", 1488 RelationName: "view", 1489 IsPermission: true, 1490 }, 1491 { 1492 DefinitionName: "document", 1493 RelationName: "viewer", 1494 IsPermission: false, 1495 }, 1496 }, 1497 }, 1498 { 1499 name: "filtered", 1500 schema: ` 1501 definition user {} 1502 1503 definition folder { 1504 relation viewer: user 1505 } 1506 1507 definition document { 1508 relation unused: user 1509 relation editor: user 1510 relation viewer: user 1511 permission view = viewer + editor 1512 permission another = unused 1513 }`, 1514 definitionName: "user", 1515 relationName: "", 1516 filter: "folder", 1517 expectedResponse: []*v1.ExpRelationReference{ 1518 { 1519 DefinitionName: "folder", 1520 RelationName: "viewer", 1521 IsPermission: false, 1522 }, 1523 }, 1524 }, 1525 { 1526 name: "basic relation", 1527 schema: ` 1528 definition user {} 1529 1530 definition document { 1531 relation unused: user 1532 relation editor: user 1533 relation viewer: user 1534 permission view = viewer + editor 1535 permission another = unused 1536 }`, 1537 definitionName: "document", 1538 relationName: "viewer", 1539 expectedResponse: []*v1.ExpRelationReference{ 1540 { 1541 DefinitionName: "document", 1542 RelationName: "view", 1543 IsPermission: true, 1544 }, 1545 }, 1546 }, 1547 { 1548 name: "multiple permissions", 1549 schema: ` 1550 definition user {} 1551 1552 definition document { 1553 relation unused: user 1554 relation editor: user 1555 relation viewer: user 1556 permission view = viewer + editor 1557 permission only_view = viewer 1558 permission another = unused 1559 }`, 1560 definitionName: "document", 1561 relationName: "viewer", 1562 expectedResponse: []*v1.ExpRelationReference{ 1563 { 1564 DefinitionName: "document", 1565 RelationName: "only_view", 1566 IsPermission: true, 1567 }, 1568 { 1569 DefinitionName: "document", 1570 RelationName: "view", 1571 IsPermission: true, 1572 }, 1573 }, 1574 }, 1575 { 1576 name: "empty response", 1577 schema: ` 1578 definition user {} 1579 1580 definition document { 1581 relation unused: user 1582 permission empty = nil 1583 } 1584 `, 1585 definitionName: "document", 1586 relationName: "unused", 1587 expectedResponse: []*v1.ExpRelationReference{}, 1588 }, 1589 } 1590 1591 for _, tc := range tcs { 1592 tc := tc 1593 t.Run(tc.name, func(t *testing.T) { 1594 conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore) 1595 expClient := v1.NewExperimentalServiceClient(conn) 1596 schemaClient := v1.NewSchemaServiceClient(conn) 1597 defer cleanup() 1598 1599 // Write the schema. 1600 _, err := schemaClient.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ 1601 Schema: tc.schema, 1602 }) 1603 require.NoError(t, err) 1604 1605 actual, err := expClient.ExperimentalComputablePermissions(context.Background(), &v1.ExperimentalComputablePermissionsRequest{ 1606 DefinitionName: tc.definitionName, 1607 RelationName: tc.relationName, 1608 OptionalDefinitionNameFilter: tc.filter, 1609 Consistency: &v1.Consistency{ 1610 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 1611 }, 1612 }) 1613 1614 if tc.expectedError != "" { 1615 require.Error(t, err) 1616 require.Contains(t, err.Error(), tc.expectedError) 1617 grpcutil.RequireStatus(t, tc.expectedCode, err) 1618 } else { 1619 require.NoError(t, err) 1620 require.NotNil(t, actual.ReadAt) 1621 actual.ReadAt = nil 1622 1623 testutil.RequireProtoEqual(t, &v1.ExperimentalComputablePermissionsResponse{ 1624 Permissions: tc.expectedResponse, 1625 }, actual, "mismatch in response") 1626 } 1627 }) 1628 } 1629 }