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  }