github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/permissions_test.go (about)

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