github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/dispatch/graph/lookupsubjects_test.go (about)

     1  package graph
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sort"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/require"
    10  	"go.uber.org/goleak"
    11  
    12  	"github.com/authzed/spicedb/internal/caveats"
    13  	"github.com/authzed/spicedb/internal/datastore/common"
    14  	"github.com/authzed/spicedb/internal/datastore/memdb"
    15  	"github.com/authzed/spicedb/internal/dispatch"
    16  	log "github.com/authzed/spicedb/internal/logging"
    17  	datastoremw "github.com/authzed/spicedb/internal/middleware/datastore"
    18  	"github.com/authzed/spicedb/internal/testfixtures"
    19  	itestutil "github.com/authzed/spicedb/internal/testutil"
    20  	corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
    21  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    22  	"github.com/authzed/spicedb/pkg/tuple"
    23  )
    24  
    25  var (
    26  	caveatexpr   = caveats.CaveatExprForTesting
    27  	caveatAnd    = caveats.And
    28  	caveatOr     = caveats.Or
    29  	caveatInvert = caveats.Invert
    30  )
    31  
    32  func TestSimpleLookupSubjects(t *testing.T) {
    33  	defer goleak.VerifyNone(t, goleakIgnores...)
    34  
    35  	testCases := []struct {
    36  		resourceType     string
    37  		resourceID       string
    38  		permission       string
    39  		subjectType      string
    40  		subjectRelation  string
    41  		expectedSubjects []string
    42  	}{
    43  		{
    44  			"document",
    45  			"masterplan",
    46  			"view",
    47  			"user",
    48  			"...",
    49  			[]string{"auditor", "chief_financial_officer", "eng_lead", "legal", "owner", "product_manager", "vp_product"},
    50  		},
    51  		{
    52  			"document",
    53  			"masterplan",
    54  			"edit",
    55  			"user",
    56  			"...",
    57  			[]string{"product_manager"},
    58  		},
    59  		{
    60  			"document",
    61  			"masterplan",
    62  			"view_and_edit",
    63  			"user",
    64  			"...",
    65  			[]string{},
    66  		},
    67  		{
    68  			"document",
    69  			"specialplan",
    70  			"view",
    71  			"user",
    72  			"...",
    73  			[]string{"multiroleguy"},
    74  		},
    75  		{
    76  			"document",
    77  			"specialplan",
    78  			"edit",
    79  			"user",
    80  			"...",
    81  			[]string{"multiroleguy"},
    82  		},
    83  		{
    84  			"document",
    85  			"specialplan",
    86  			"viewer_and_editor",
    87  			"user",
    88  			"...",
    89  			[]string{"multiroleguy", "missingrolegal"},
    90  		},
    91  		{
    92  			"document",
    93  			"specialplan",
    94  			"view_and_edit",
    95  			"user",
    96  			"...",
    97  			[]string{"multiroleguy"},
    98  		},
    99  		{
   100  			"folder",
   101  			"company",
   102  			"view",
   103  			"user",
   104  			"...",
   105  			[]string{"auditor", "legal", "owner"},
   106  		},
   107  		{
   108  			"folder",
   109  			"strategy",
   110  			"view",
   111  			"user",
   112  			"...",
   113  			[]string{"auditor", "legal", "owner", "vp_product"},
   114  		},
   115  		{
   116  			"document",
   117  			"masterplan",
   118  			"parent",
   119  			"folder",
   120  			"...",
   121  			[]string{"plans", "strategy"},
   122  		},
   123  		{
   124  			"document",
   125  			"masterplan",
   126  			"view",
   127  			"folder",
   128  			"...",
   129  			[]string{},
   130  		},
   131  	}
   132  
   133  	for _, tc := range testCases {
   134  		tc := tc
   135  		t.Run(fmt.Sprintf("simple-lookup-subjects:%s:%s:%s:%s:%s", tc.resourceType, tc.resourceID, tc.permission, tc.subjectType, tc.subjectRelation), func(t *testing.T) {
   136  			defer goleak.VerifyNone(t, goleak.IgnoreCurrent())
   137  
   138  			require := require.New(t)
   139  
   140  			ctx, dis, revision := newLocalDispatcher(t)
   141  			defer dis.Close()
   142  
   143  			stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx)
   144  
   145  			err := dis.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{
   146  				ResourceRelation: RR(tc.resourceType, tc.permission),
   147  				ResourceIds:      []string{tc.resourceID},
   148  				SubjectRelation:  RR(tc.subjectType, tc.subjectRelation),
   149  				Metadata: &v1.ResolverMeta{
   150  					AtRevision:     revision.String(),
   151  					DepthRemaining: 50,
   152  				},
   153  			}, stream)
   154  
   155  			require.NoError(err)
   156  
   157  			foundSubjectIds := []string{}
   158  			for _, result := range stream.Results() {
   159  				results, ok := result.FoundSubjectsByResourceId[tc.resourceID]
   160  				if ok {
   161  					for _, found := range results.FoundSubjects {
   162  						if len(found.ExcludedSubjects) > 0 {
   163  							continue
   164  						}
   165  
   166  						foundSubjectIds = append(foundSubjectIds, found.SubjectId)
   167  					}
   168  				}
   169  			}
   170  
   171  			sort.Strings(foundSubjectIds)
   172  			sort.Strings(tc.expectedSubjects)
   173  			require.Equal(tc.expectedSubjects, foundSubjectIds)
   174  
   175  			// Ensure every subject found has access.
   176  			for _, subjectID := range foundSubjectIds {
   177  				checkResult, err := dis.DispatchCheck(ctx, &v1.DispatchCheckRequest{
   178  					ResourceRelation: RR(tc.resourceType, tc.permission),
   179  					ResourceIds:      []string{tc.resourceID},
   180  					ResultsSetting:   v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT,
   181  					Subject:          ONR(tc.subjectType, subjectID, tc.subjectRelation),
   182  					Metadata: &v1.ResolverMeta{
   183  						AtRevision:     revision.String(),
   184  						DepthRemaining: 50,
   185  					},
   186  				})
   187  
   188  				require.NoError(err)
   189  				require.Equal(v1.ResourceCheckResult_MEMBER, checkResult.ResultsByResourceId[tc.resourceID].Membership)
   190  			}
   191  			dis.Close()
   192  		})
   193  	}
   194  }
   195  
   196  func TestLookupSubjectsMaxDepth(t *testing.T) {
   197  	require := require.New(t)
   198  
   199  	rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   200  	require.NoError(err)
   201  
   202  	ds, _ := testfixtures.StandardDatastoreWithSchema(rawDS, require)
   203  
   204  	ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background()))
   205  	require.NoError(datastoremw.SetInContext(ctx, ds))
   206  
   207  	tpl := tuple.Parse("folder:oops#parent@folder:oops")
   208  	revision, err := common.WriteTuples(ctx, ds, corev1.RelationTupleUpdate_CREATE, tpl)
   209  	require.NoError(err)
   210  
   211  	dis := NewLocalOnlyDispatcher(10)
   212  	stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx)
   213  
   214  	err = dis.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{
   215  		ResourceRelation: RR("folder", "view"),
   216  		ResourceIds:      []string{"oops"},
   217  		SubjectRelation:  RR("user", "..."),
   218  		Metadata: &v1.ResolverMeta{
   219  			AtRevision:     revision.String(),
   220  			DepthRemaining: 50,
   221  		},
   222  	}, stream)
   223  	require.Error(err)
   224  }
   225  
   226  func TestLookupSubjectsDispatchCount(t *testing.T) {
   227  	testCases := []struct {
   228  		resourceType          string
   229  		resourceID            string
   230  		permission            string
   231  		subjectType           string
   232  		subjectRelation       string
   233  		expectedDispatchCount int
   234  	}{
   235  		{
   236  			"document",
   237  			"masterplan",
   238  			"view",
   239  			"user",
   240  			"...",
   241  			13,
   242  		},
   243  		{
   244  			"document",
   245  			"masterplan",
   246  			"view_and_edit",
   247  			"user",
   248  			"...",
   249  			5,
   250  		},
   251  	}
   252  
   253  	for _, tc := range testCases {
   254  		tc := tc
   255  		t.Run(fmt.Sprintf("dispatch-count-lookup-subjects:%s:%s:%s:%s:%s", tc.resourceType, tc.resourceID, tc.permission, tc.subjectType, tc.subjectRelation), func(t *testing.T) {
   256  			require := require.New(t)
   257  
   258  			ctx, dis, revision := newLocalDispatcher(t)
   259  			stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx)
   260  
   261  			err := dis.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{
   262  				ResourceRelation: RR(tc.resourceType, tc.permission),
   263  				ResourceIds:      []string{tc.resourceID},
   264  				SubjectRelation:  RR(tc.subjectType, tc.subjectRelation),
   265  				Metadata: &v1.ResolverMeta{
   266  					AtRevision:     revision.String(),
   267  					DepthRemaining: 50,
   268  				},
   269  			}, stream)
   270  
   271  			require.NoError(err)
   272  			for _, result := range stream.Results() {
   273  				require.LessOrEqual(int(result.Metadata.DispatchCount), tc.expectedDispatchCount, "Found dispatch count greater than expected")
   274  			}
   275  		})
   276  	}
   277  }
   278  
   279  func TestCaveatedLookupSubjects(t *testing.T) {
   280  	testCases := []struct {
   281  		name          string
   282  		schema        string
   283  		relationships []*corev1.RelationTuple
   284  		start         *corev1.ObjectAndRelation
   285  		target        *corev1.RelationReference
   286  		expected      []*v1.FoundSubject
   287  	}{
   288  		{
   289  			"basic caveated",
   290  			`definition user {}
   291  		
   292  			 caveat somecaveat(somecondition int) {
   293  				somecondition == 42
   294  			 }
   295  
   296  		 	 definition document {
   297  				relation viewer: user | user with somecaveat
   298  				permission view = viewer
   299    		 }`,
   300  			[]*corev1.RelationTuple{
   301  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"),
   302  				tuple.MustParse("document:first#viewer@user:sarah"),
   303  			},
   304  			ONR("document", "first", "view"),
   305  			RR("user", "..."),
   306  			[]*v1.FoundSubject{
   307  				{
   308  					SubjectId: "sarah",
   309  				},
   310  				{
   311  					SubjectId:        "tom",
   312  					CaveatExpression: caveatexpr("somecaveat"),
   313  				},
   314  			},
   315  		},
   316  		{
   317  			"union caveated",
   318  			`definition user {}
   319  		
   320  			 caveat somecaveat(somecondition int) {
   321  				somecondition == 42
   322  			 }
   323  
   324  		 	 definition document {
   325  				relation viewer: user | user with somecaveat
   326  				relation editor: user | user with somecaveat
   327  				permission view = viewer + editor
   328    		 }`,
   329  			[]*corev1.RelationTuple{
   330  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"),
   331  				tuple.MustWithCaveat(tuple.MustParse("document:first#editor@user:tom"), "somecaveat"),
   332  			},
   333  			ONR("document", "first", "view"),
   334  			RR("user", "..."),
   335  			[]*v1.FoundSubject{
   336  				{
   337  					SubjectId:        "tom",
   338  					CaveatExpression: caveatexpr("somecaveat"),
   339  				},
   340  			},
   341  		},
   342  		{
   343  			"union short-circuited caveated",
   344  			`definition user {}
   345  		
   346  			 caveat somecaveat(somecondition int) {
   347  				somecondition == 42
   348  			 }
   349  
   350  		 	 definition document {
   351  				relation viewer: user | user with somecaveat
   352  				relation editor: user | user with somecaveat
   353  				permission view = viewer + editor
   354    		 }`,
   355  			[]*corev1.RelationTuple{
   356  				tuple.MustParse("document:first#viewer@user:tom"),
   357  				tuple.MustWithCaveat(tuple.MustParse("document:first#editor@user:tom"), "somecaveat"),
   358  			},
   359  			ONR("document", "first", "view"),
   360  			RR("user", "..."),
   361  			[]*v1.FoundSubject{
   362  				{
   363  					SubjectId: "tom",
   364  				},
   365  			},
   366  		},
   367  		{
   368  			"intersection caveated",
   369  			`definition user {}
   370  		
   371  			 caveat somecaveat(somecondition int) {
   372  				somecondition == 42
   373  			 }
   374  
   375  			 caveat anothercaveat(somecondition int) {
   376  				somecondition == 42
   377  			 }
   378  
   379  		 	 definition document {
   380  				relation viewer: user | user with somecaveat
   381  				relation editor: user | user with anothercaveat
   382  				permission view = viewer & editor
   383    		 }`,
   384  			[]*corev1.RelationTuple{
   385  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"),
   386  				tuple.MustWithCaveat(tuple.MustParse("document:first#editor@user:tom"), "anothercaveat"),
   387  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:sarah"), "somecaveat"),
   388  			},
   389  			ONR("document", "first", "view"),
   390  			RR("user", "..."),
   391  			[]*v1.FoundSubject{
   392  				{
   393  					SubjectId: "tom",
   394  					CaveatExpression: caveatAnd(
   395  						caveatexpr("somecaveat"),
   396  						caveatexpr("anothercaveat"),
   397  					),
   398  				},
   399  			},
   400  		},
   401  		{
   402  			"exclusion caveated",
   403  			`definition user {}
   404  		
   405  			 caveat somecaveat(somecondition int) {
   406  				somecondition == 42
   407  			 }
   408  
   409  			 caveat anothercaveat(somecondition int) {
   410  				somecondition == 42
   411  			 }
   412  
   413  		 	 definition document {
   414  				relation viewer: user | user with somecaveat
   415  				relation banned: user | user with anothercaveat
   416  				permission view = viewer - banned
   417    		 }`,
   418  			[]*corev1.RelationTuple{
   419  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"),
   420  				tuple.MustWithCaveat(tuple.MustParse("document:first#banned@user:tom"), "anothercaveat"),
   421  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:sarah"), "somecaveat"),
   422  			},
   423  			ONR("document", "first", "view"),
   424  			RR("user", "..."),
   425  			[]*v1.FoundSubject{
   426  				{
   427  					SubjectId:        "sarah",
   428  					CaveatExpression: caveatexpr("somecaveat"),
   429  				},
   430  				{
   431  					SubjectId: "tom",
   432  					CaveatExpression: caveatAnd(
   433  						caveatexpr("somecaveat"),
   434  						caveatInvert(caveatexpr("anothercaveat")),
   435  					),
   436  				},
   437  			},
   438  		},
   439  		{
   440  			"arrow caveated",
   441  			`definition user {}
   442  		
   443  			 caveat somecaveat(somecondition int) {
   444  				somecondition == 42
   445  			 }
   446  
   447  			 definition org {
   448  				relation viewer: user								
   449  			 }
   450  
   451  		 	 definition document {
   452  				relation org: org with somecaveat
   453  				permission view = org->viewer
   454    		 }`,
   455  			[]*corev1.RelationTuple{
   456  				tuple.MustWithCaveat(tuple.MustParse("document:first#org@org:someorg"), "somecaveat"),
   457  				tuple.MustParse("org:someorg#viewer@user:tom"),
   458  			},
   459  			ONR("document", "first", "view"),
   460  			RR("user", "..."),
   461  			[]*v1.FoundSubject{
   462  				{
   463  					SubjectId:        "tom",
   464  					CaveatExpression: caveatexpr("somecaveat"),
   465  				},
   466  			},
   467  		},
   468  		{
   469  			"arrow and relation caveated",
   470  			`definition user {}
   471  		
   472  			 caveat somecaveat(somecondition int) {
   473  				somecondition == 42
   474  			 }
   475  
   476  			 caveat anothercaveat(somecondition int) {
   477  				somecondition == 42
   478  			 }
   479  
   480  			 definition org {
   481  				relation viewer: user with anothercaveat					
   482  			 }
   483  
   484  		 	 definition document {
   485  				relation org: org with somecaveat
   486  				permission view = org->viewer
   487    		 }`,
   488  			[]*corev1.RelationTuple{
   489  				tuple.MustWithCaveat(tuple.MustParse("document:first#org@org:someorg"), "somecaveat"),
   490  				tuple.MustWithCaveat(tuple.MustParse("org:someorg#viewer@user:tom"), "anothercaveat"),
   491  			},
   492  			ONR("document", "first", "view"),
   493  			RR("user", "..."),
   494  			[]*v1.FoundSubject{
   495  				{
   496  					SubjectId: "tom",
   497  					CaveatExpression: caveatAnd(
   498  						caveatexpr("somecaveat"),
   499  						caveatexpr("anothercaveat"),
   500  					),
   501  				},
   502  			},
   503  		},
   504  		{
   505  			"caveated wildcard with exclusions caveated",
   506  			`definition user {}
   507  		
   508  			 caveat somecaveat(somecondition int) {
   509  				somecondition == 42
   510  			 }
   511  
   512  			 caveat anothercaveat(somecondition int) {
   513  				somecondition == 42
   514  			 }
   515  
   516  		 	 definition document {
   517  				relation viewer: user:* with somecaveat
   518  				relation banned: user with anothercaveat
   519  				permission view = viewer - banned
   520    		 }`,
   521  			[]*corev1.RelationTuple{
   522  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:*"), "somecaveat"),
   523  				tuple.MustWithCaveat(tuple.MustParse("document:first#banned@user:tom"), "anothercaveat"),
   524  			},
   525  			ONR("document", "first", "view"),
   526  			RR("user", "..."),
   527  			[]*v1.FoundSubject{
   528  				{
   529  					SubjectId: "*",
   530  					ExcludedSubjects: []*v1.FoundSubject{
   531  						{
   532  							SubjectId:        "tom",
   533  							CaveatExpression: caveatexpr("anothercaveat"),
   534  						},
   535  					},
   536  					CaveatExpression: caveatexpr("somecaveat"),
   537  				},
   538  			},
   539  		},
   540  		{
   541  			"caveated wildcard with exclusions caveated",
   542  			`definition user {}
   543  		
   544  			 caveat somecaveat(somecondition int) {
   545  				somecondition == 42
   546  			 }
   547  
   548  			 caveat anothercaveat(somecondition int) {
   549  				somecondition == 42
   550  			 }
   551  
   552  			 caveat thirdcaveat(somecondition int) {
   553  				somecondition == 42
   554  			 }
   555  
   556  		 	 definition document {
   557  				relation viewer: user:* with somecaveat
   558  				relation banned: user with anothercaveat
   559  				relation explicitly_allowed: user with thirdcaveat
   560  				permission view = (viewer - banned) + explicitly_allowed
   561    		 }`,
   562  			[]*corev1.RelationTuple{
   563  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:*"), "somecaveat"),
   564  				tuple.MustWithCaveat(tuple.MustParse("document:first#banned@user:tom"), "anothercaveat"),
   565  				tuple.MustWithCaveat(tuple.MustParse("document:first#explicitly_allowed@user:tom"), "thirdcaveat"),
   566  			},
   567  			ONR("document", "first", "view"),
   568  			RR("user", "..."),
   569  			[]*v1.FoundSubject{
   570  				{
   571  					SubjectId:        "tom",
   572  					CaveatExpression: caveatexpr("thirdcaveat"),
   573  				},
   574  				{
   575  					SubjectId: "*",
   576  					ExcludedSubjects: []*v1.FoundSubject{
   577  						{
   578  							SubjectId: "tom",
   579  							CaveatExpression: caveatAnd(
   580  								caveatexpr("anothercaveat"),
   581  								caveatInvert(caveatexpr("thirdcaveat")),
   582  							),
   583  						},
   584  					},
   585  					CaveatExpression: caveatexpr("somecaveat"),
   586  				},
   587  			},
   588  		},
   589  		{
   590  			"multiple via arrows",
   591  			`definition user {}
   592  		
   593  			 caveat somecaveat(somecondition int) {
   594  				somecondition == 42
   595  			 }
   596  
   597  			 caveat anothercaveat(somecondition int) {
   598  				somecondition == 42
   599  			 }
   600  
   601  			 caveat thirdcaveat(somecondition int) {
   602  				somecondition == 42
   603  			 }
   604  
   605  			 definition org {
   606  				relation viewer: user | user with anothercaveat				
   607  			 }
   608  
   609  		 	 definition document {
   610  				relation org: org with somecaveat | org with thirdcaveat
   611  				permission view = org->viewer
   612    		 }`,
   613  			[]*corev1.RelationTuple{
   614  				tuple.MustWithCaveat(tuple.MustParse("document:first#org@org:someorg"), "somecaveat"),
   615  				tuple.MustWithCaveat(tuple.MustParse("org:someorg#viewer@user:tom"), "anothercaveat"),
   616  				tuple.MustParse("org:someorg#viewer@user:sarah"),
   617  
   618  				tuple.MustWithCaveat(tuple.MustParse("document:first#org@org:anotherorg"), "thirdcaveat"),
   619  				tuple.MustWithCaveat(tuple.MustParse("org:anotherorg#viewer@user:amy"), "anothercaveat"),
   620  			},
   621  			ONR("document", "first", "view"),
   622  			RR("user", "..."),
   623  			[]*v1.FoundSubject{
   624  				{
   625  					SubjectId: "tom",
   626  					CaveatExpression: caveatAnd(
   627  						caveatexpr("somecaveat"),
   628  						caveatexpr("anothercaveat"),
   629  					),
   630  				},
   631  				{
   632  					SubjectId:        "sarah",
   633  					CaveatExpression: caveatexpr("somecaveat"),
   634  				},
   635  				{
   636  					SubjectId: "amy",
   637  					CaveatExpression: caveatAnd(
   638  						caveatexpr("thirdcaveat"),
   639  						caveatexpr("anothercaveat"),
   640  					),
   641  				},
   642  			},
   643  		},
   644  		{
   645  			"arrow over different relations of the same subject",
   646  			`definition user {}
   647  	
   648  			 definition folder {
   649  				relation parent: folder
   650  				relation viewer: user
   651  				permission view = viewer
   652  			 }
   653  
   654  		 	 definition document {
   655  				relation folder: folder | folder#parent
   656  				permission view = folder->view
   657    		 }`,
   658  			[]*corev1.RelationTuple{
   659  				tuple.MustParse("folder:folder1#viewer@user:tom"),
   660  				tuple.MustParse("folder:folder2#viewer@user:fred"),
   661  				tuple.MustParse("document:somedoc#folder@folder:folder1"),
   662  				tuple.MustParse("document:somedoc#folder@folder:folder2#parent"),
   663  			},
   664  			ONR("document", "somedoc", "view"),
   665  			RR("user", "..."),
   666  			[]*v1.FoundSubject{
   667  				{
   668  					SubjectId: "tom",
   669  				},
   670  				{
   671  					SubjectId: "fred",
   672  				},
   673  			},
   674  		},
   675  		{
   676  			"caveated arrow over different relations of the same subject",
   677  			`definition user {}
   678  	
   679  			 caveat somecaveat(somecondition int) {
   680  				somecondition == 42
   681  			 }
   682  
   683  			 definition folder {
   684  				relation parent: folder
   685  				relation viewer: user
   686  				permission view = viewer
   687  			 }
   688  
   689  		 	 definition document {
   690  				relation folder: folder | folder#parent with somecaveat
   691  				permission view = folder->view
   692    		 }`,
   693  			[]*corev1.RelationTuple{
   694  				tuple.MustParse("folder:folder1#viewer@user:tom"),
   695  				tuple.MustParse("folder:folder2#viewer@user:fred"),
   696  				tuple.MustParse("document:somedoc#folder@folder:folder1"),
   697  				tuple.MustWithCaveat(tuple.MustParse("document:somedoc#folder@folder:folder2#parent"), "somecaveat"),
   698  			},
   699  			ONR("document", "somedoc", "view"),
   700  			RR("user", "..."),
   701  			[]*v1.FoundSubject{
   702  				{
   703  					SubjectId: "tom",
   704  				},
   705  				{
   706  					SubjectId:        "fred",
   707  					CaveatExpression: caveatexpr("somecaveat"),
   708  				},
   709  			},
   710  		},
   711  	}
   712  
   713  	for _, tc := range testCases {
   714  		tc := tc
   715  		t.Run(tc.name, func(t *testing.T) {
   716  			require := require.New(t)
   717  
   718  			dispatcher := NewLocalOnlyDispatcher(10)
   719  
   720  			ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   721  			require.NoError(err)
   722  
   723  			ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships(ds, tc.schema, tc.relationships, require)
   724  
   725  			ctx := datastoremw.ContextWithHandle(context.Background())
   726  			require.NoError(datastoremw.SetInContext(ctx, ds))
   727  
   728  			stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx)
   729  			err = dispatcher.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{
   730  				ResourceRelation: &corev1.RelationReference{
   731  					Namespace: tc.start.Namespace,
   732  					Relation:  tc.start.Relation,
   733  				},
   734  				ResourceIds:     []string{tc.start.ObjectId},
   735  				SubjectRelation: tc.target,
   736  				Metadata: &v1.ResolverMeta{
   737  					AtRevision:     revision.String(),
   738  					DepthRemaining: 50,
   739  				},
   740  			}, stream)
   741  			require.NoError(err)
   742  
   743  			results := []*v1.FoundSubject{}
   744  			for _, streamResult := range stream.Results() {
   745  				for _, foundSubjects := range streamResult.FoundSubjectsByResourceId {
   746  					results = append(results, foundSubjects.FoundSubjects...)
   747  				}
   748  			}
   749  
   750  			itestutil.RequireEquivalentSets(t, tc.expected, results)
   751  		})
   752  	}
   753  }
   754  
   755  func TestCursoredLookupSubjects(t *testing.T) {
   756  	testCases := []struct {
   757  		name          string
   758  		pageSizes     []int
   759  		schema        string
   760  		relationships []*corev1.RelationTuple
   761  		start         *corev1.ObjectAndRelation
   762  		target        *corev1.RelationReference
   763  		expected      []*v1.FoundSubject
   764  	}{
   765  		{
   766  			"simple",
   767  			[]int{0, 1, 2, 5, 100},
   768  			`definition user {}
   769  
   770  		 	 definition document {
   771  				relation viewer: user
   772  				permission view = viewer
   773    		 }`,
   774  			[]*corev1.RelationTuple{
   775  				tuple.MustParse("document:first#viewer@user:sarah"),
   776  				tuple.MustParse("document:first#viewer@user:fred"),
   777  				tuple.MustParse("document:first#viewer@user:tom"),
   778  				tuple.MustParse("document:first#viewer@user:andria"),
   779  				tuple.MustParse("document:first#viewer@user:victor"),
   780  				tuple.MustParse("document:first#viewer@user:chuck"),
   781  				tuple.MustParse("document:first#viewer@user:ben"),
   782  			},
   783  			ONR("document", "first", "view"),
   784  			RR("user", "..."),
   785  			[]*v1.FoundSubject{
   786  				{SubjectId: "sarah"},
   787  				{SubjectId: "fred"},
   788  				{SubjectId: "tom"},
   789  				{SubjectId: "andria"},
   790  				{SubjectId: "victor"},
   791  				{SubjectId: "chuck"},
   792  				{SubjectId: "ben"},
   793  			},
   794  		},
   795  		{
   796  			"basic union",
   797  			[]int{0, 1, 2, 5, 100},
   798  			`definition user {}
   799  
   800  		 	 definition document {
   801  				relation viewer1: user
   802  				relation viewer2: user
   803  				permission view = viewer1 + viewer2
   804    		 }`,
   805  			[]*corev1.RelationTuple{
   806  				tuple.MustParse("document:first#viewer1@user:sarah"),
   807  				tuple.MustParse("document:first#viewer1@user:fred"),
   808  				tuple.MustParse("document:first#viewer1@user:tom"),
   809  				tuple.MustParse("document:first#viewer2@user:andria"),
   810  				tuple.MustParse("document:first#viewer2@user:victor"),
   811  				tuple.MustParse("document:first#viewer2@user:chuck"),
   812  				tuple.MustParse("document:first#viewer2@user:ben"),
   813  			},
   814  			ONR("document", "first", "view"),
   815  			RR("user", "..."),
   816  			[]*v1.FoundSubject{
   817  				{SubjectId: "sarah"},
   818  				{SubjectId: "fred"},
   819  				{SubjectId: "tom"},
   820  				{SubjectId: "andria"},
   821  				{SubjectId: "victor"},
   822  				{SubjectId: "chuck"},
   823  				{SubjectId: "ben"},
   824  			},
   825  		},
   826  		{
   827  			"basic intersection",
   828  			[]int{0, 1, 2, 5, 100},
   829  			`definition user {}
   830  
   831  		 	 definition document {
   832  				relation viewer1: user
   833  				relation viewer2: user
   834  				permission view = viewer1 & viewer2
   835    		 }`,
   836  			[]*corev1.RelationTuple{
   837  				tuple.MustParse("document:first#viewer1@user:sarah"),
   838  				tuple.MustParse("document:first#viewer1@user:fred"),
   839  				tuple.MustParse("document:first#viewer1@user:tom"),
   840  				tuple.MustParse("document:first#viewer1@user:andria"),
   841  				tuple.MustParse("document:first#viewer1@user:victor"),
   842  				tuple.MustParse("document:first#viewer2@user:victor"),
   843  				tuple.MustParse("document:first#viewer2@user:chuck"),
   844  				tuple.MustParse("document:first#viewer2@user:ben"),
   845  				tuple.MustParse("document:first#viewer2@user:andria"),
   846  			},
   847  			ONR("document", "first", "view"),
   848  			RR("user", "..."),
   849  			[]*v1.FoundSubject{
   850  				{SubjectId: "andria"},
   851  				{SubjectId: "victor"},
   852  			},
   853  		},
   854  		{
   855  			"basic exclusion",
   856  			[]int{0, 1, 2, 5, 100},
   857  			`definition user {}
   858  
   859  		 	 definition document {
   860  				relation viewer1: user
   861  				relation viewer2: user
   862  				permission view = viewer1 - viewer2
   863    		 }`,
   864  			[]*corev1.RelationTuple{
   865  				tuple.MustParse("document:first#viewer1@user:sarah"),
   866  				tuple.MustParse("document:first#viewer1@user:fred"),
   867  				tuple.MustParse("document:first#viewer1@user:tom"),
   868  				tuple.MustParse("document:first#viewer1@user:andria"),
   869  				tuple.MustParse("document:first#viewer1@user:victor"),
   870  				tuple.MustParse("document:first#viewer2@user:victor"),
   871  				tuple.MustParse("document:first#viewer2@user:chuck"),
   872  				tuple.MustParse("document:first#viewer2@user:ben"),
   873  				tuple.MustParse("document:first#viewer2@user:andria"),
   874  			},
   875  			ONR("document", "first", "view"),
   876  			RR("user", "..."),
   877  			[]*v1.FoundSubject{
   878  				{SubjectId: "sarah"},
   879  				{SubjectId: "fred"},
   880  				{SubjectId: "tom"},
   881  			},
   882  		},
   883  		{
   884  			"union over exclusion",
   885  			[]int{0, 1, 2, 5, 100},
   886  			`definition user {}
   887  
   888  		 	 definition document {
   889  				relation viewer: user
   890  				relation editor: user
   891  				relation banned: user
   892  
   893  				permission edit = editor - banned
   894  				permission view = viewer + edit
   895    		 }`,
   896  			[]*corev1.RelationTuple{
   897  				tuple.MustParse("document:first#viewer@user:sarah"),
   898  				tuple.MustParse("document:first#viewer@user:fred"),
   899  
   900  				tuple.MustParse("document:first#editor@user:sarah"),
   901  				tuple.MustParse("document:first#editor@user:george"),
   902  				tuple.MustParse("document:first#editor@user:victor"),
   903  
   904  				tuple.MustParse("document:first#banned@user:victor"),
   905  				tuple.MustParse("document:first#banned@user:bannedguy"),
   906  			},
   907  			ONR("document", "first", "view"),
   908  			RR("user", "..."),
   909  			[]*v1.FoundSubject{
   910  				{SubjectId: "sarah"},
   911  				{SubjectId: "fred"},
   912  				{SubjectId: "george"},
   913  			},
   914  		},
   915  		{
   916  			"basic caveated",
   917  			[]int{0, 1, 2, 5, 100},
   918  			`definition user {}
   919  		
   920  			 caveat somecaveat(somecondition int) {
   921  				somecondition == 42
   922  			 }
   923  
   924  		 	 definition document {
   925  				relation viewer: user | user with somecaveat
   926  				permission view = viewer
   927    		 }`,
   928  			[]*corev1.RelationTuple{
   929  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"),
   930  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:fred"), "somecaveat"),
   931  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:sarah"), "somecaveat"),
   932  				tuple.MustParse("document:first#viewer@user:tracy"),
   933  			},
   934  			ONR("document", "first", "view"),
   935  			RR("user", "..."),
   936  			[]*v1.FoundSubject{
   937  				{
   938  					SubjectId: "tracy",
   939  				},
   940  				{
   941  					SubjectId:        "tom",
   942  					CaveatExpression: caveatexpr("somecaveat"),
   943  				},
   944  				{
   945  					SubjectId:        "fred",
   946  					CaveatExpression: caveatexpr("somecaveat"),
   947  				},
   948  				{
   949  					SubjectId:        "sarah",
   950  					CaveatExpression: caveatexpr("somecaveat"),
   951  				},
   952  			},
   953  		},
   954  		{
   955  			"union short-circuited caveated",
   956  			[]int{0, 1, 2, 5, 100},
   957  			`definition user {}
   958  		
   959  			 caveat somecaveat(somecondition int) {
   960  				somecondition == 42
   961  			 }
   962  
   963  		 	 definition document {
   964  				relation viewer: user | user with somecaveat
   965  				relation editor: user | user with somecaveat
   966  				permission view = viewer + editor
   967    		 }`,
   968  			[]*corev1.RelationTuple{
   969  				tuple.MustParse("document:first#viewer@user:tom"),
   970  				tuple.MustWithCaveat(tuple.MustParse("document:first#editor@user:tom"), "somecaveat"),
   971  			},
   972  			ONR("document", "first", "view"),
   973  			RR("user", "..."),
   974  			[]*v1.FoundSubject{
   975  				{
   976  					SubjectId: "tom",
   977  				},
   978  			},
   979  		},
   980  		{
   981  			"intersection caveated",
   982  			[]int{0, 1, 2, 5, 100},
   983  			`definition user {}
   984  		
   985  			 caveat somecaveat(somecondition int) {
   986  				somecondition == 42
   987  			 }
   988  
   989  			 caveat anothercaveat(somecondition int) {
   990  				somecondition == 42
   991  			 }
   992  
   993  		 	 definition document {
   994  				relation viewer: user | user with somecaveat
   995  				relation editor: user | user with anothercaveat
   996  				permission view = viewer & editor
   997    		 }`,
   998  			[]*corev1.RelationTuple{
   999  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"),
  1000  				tuple.MustWithCaveat(tuple.MustParse("document:first#editor@user:tom"), "anothercaveat"),
  1001  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:sarah"), "somecaveat"),
  1002  			},
  1003  			ONR("document", "first", "view"),
  1004  			RR("user", "..."),
  1005  			[]*v1.FoundSubject{
  1006  				{
  1007  					SubjectId: "tom",
  1008  					CaveatExpression: caveatAnd(
  1009  						caveatexpr("somecaveat"),
  1010  						caveatexpr("anothercaveat"),
  1011  					),
  1012  				},
  1013  			},
  1014  		},
  1015  		{
  1016  			"simple wildcard",
  1017  			[]int{0, 1, 2, 5, 100},
  1018  			`definition user {}
  1019  
  1020  		 	 definition document {
  1021  				relation viewer: user | user:*
  1022  				permission view = viewer
  1023    		 }`,
  1024  			[]*corev1.RelationTuple{
  1025  				tuple.MustParse("document:first#viewer@user:sarah"),
  1026  				tuple.MustParse("document:first#viewer@user:fred"),
  1027  				tuple.MustParse("document:first#viewer@user:tom"),
  1028  				tuple.MustParse("document:first#viewer@user:andria"),
  1029  				tuple.MustParse("document:first#viewer@user:victor"),
  1030  				tuple.MustParse("document:first#viewer@user:chuck"),
  1031  				tuple.MustParse("document:first#viewer@user:ben"),
  1032  				tuple.MustParse("document:first#viewer@user:*"),
  1033  			},
  1034  			ONR("document", "first", "view"),
  1035  			RR("user", "..."),
  1036  			[]*v1.FoundSubject{
  1037  				{SubjectId: "sarah"},
  1038  				{SubjectId: "fred"},
  1039  				{SubjectId: "tom"},
  1040  				{SubjectId: "andria"},
  1041  				{SubjectId: "victor"},
  1042  				{SubjectId: "chuck"},
  1043  				{SubjectId: "ben"},
  1044  				{SubjectId: "*"},
  1045  			},
  1046  		},
  1047  		{
  1048  			"intersection with wildcard",
  1049  			[]int{0, 1, 2, 5, 100},
  1050  			`definition user {}
  1051  
  1052  		 	 definition document {
  1053  				relation viewer1: user
  1054  				relation viewer2: user:*
  1055  				permission view = viewer1 & viewer2
  1056    		 }`,
  1057  			[]*corev1.RelationTuple{
  1058  				tuple.MustParse("document:first#viewer1@user:sarah"),
  1059  				tuple.MustParse("document:first#viewer1@user:fred"),
  1060  				tuple.MustParse("document:first#viewer1@user:tom"),
  1061  				tuple.MustParse("document:first#viewer1@user:andria"),
  1062  				tuple.MustParse("document:first#viewer1@user:victor"),
  1063  				tuple.MustParse("document:first#viewer1@user:chuck"),
  1064  				tuple.MustParse("document:first#viewer1@user:ben"),
  1065  				tuple.MustParse("document:first#viewer2@user:*"),
  1066  			},
  1067  			ONR("document", "first", "view"),
  1068  			RR("user", "..."),
  1069  			[]*v1.FoundSubject{
  1070  				{SubjectId: "sarah"},
  1071  				{SubjectId: "fred"},
  1072  				{SubjectId: "tom"},
  1073  				{SubjectId: "andria"},
  1074  				{SubjectId: "victor"},
  1075  				{SubjectId: "chuck"},
  1076  				{SubjectId: "ben"},
  1077  			},
  1078  		},
  1079  		{
  1080  			"wildcard with exclusions",
  1081  			[]int{0, 1, 2, 5, 100},
  1082  			`definition user {}
  1083  
  1084  		 	 definition document {
  1085  				relation viewer: user:*
  1086  				relation banned: user
  1087  				permission view = viewer - banned
  1088    		 }`,
  1089  			[]*corev1.RelationTuple{
  1090  				tuple.MustParse("document:first#banned@user:sarah"),
  1091  				tuple.MustParse("document:first#banned@user:fred"),
  1092  				tuple.MustParse("document:first#banned@user:tom"),
  1093  				tuple.MustParse("document:first#banned@user:andria"),
  1094  				tuple.MustParse("document:first#banned@user:victor"),
  1095  				tuple.MustParse("document:first#banned@user:chuck"),
  1096  				tuple.MustParse("document:first#banned@user:ben"),
  1097  				tuple.MustParse("document:first#viewer@user:*"),
  1098  			},
  1099  			ONR("document", "first", "view"),
  1100  			RR("user", "..."),
  1101  			[]*v1.FoundSubject{
  1102  				{
  1103  					SubjectId: "*",
  1104  					ExcludedSubjects: []*v1.FoundSubject{
  1105  						{SubjectId: "sarah"},
  1106  						{SubjectId: "fred"},
  1107  						{SubjectId: "tom"},
  1108  						{SubjectId: "andria"},
  1109  						{SubjectId: "victor"},
  1110  						{SubjectId: "chuck"},
  1111  						{SubjectId: "ben"},
  1112  					},
  1113  				},
  1114  			},
  1115  		},
  1116  		{
  1117  			"canceling exclusions on wildcards",
  1118  			[]int{0, 1, 2, 5, 100},
  1119  			`definition user {}
  1120  
  1121  		 	 definition document {
  1122  				relation viewer: user
  1123  				relation banned: user:*
  1124  				relation banned2: user
  1125  				permission view = viewer - (banned - banned2)
  1126    		 }`,
  1127  			[]*corev1.RelationTuple{
  1128  				tuple.MustParse("document:first#viewer@user:sarah"),
  1129  				tuple.MustParse("document:first#viewer@user:fred"),
  1130  				tuple.MustParse("document:first#viewer@user:tom"),
  1131  				tuple.MustParse("document:first#viewer@user:andria"),
  1132  				tuple.MustParse("document:first#viewer@user:victor"),
  1133  				tuple.MustParse("document:first#viewer@user:chuck"),
  1134  				tuple.MustParse("document:first#viewer@user:ben"),
  1135  
  1136  				tuple.MustParse("document:first#banned@user:*"),
  1137  
  1138  				tuple.MustParse("document:first#banned2@user:andria"),
  1139  				tuple.MustParse("document:first#banned2@user:tom"),
  1140  			},
  1141  			ONR("document", "first", "view"),
  1142  			RR("user", "..."),
  1143  			[]*v1.FoundSubject{
  1144  				{
  1145  					SubjectId: "andria",
  1146  				},
  1147  				{
  1148  					SubjectId: "tom",
  1149  				},
  1150  			},
  1151  		},
  1152  		{
  1153  			"wildcard with many, many exclusions",
  1154  			[]int{0, 1, 2, 5, 100},
  1155  			`definition user {}
  1156  
  1157  		 	 definition document {
  1158  				relation viewer: user:*
  1159  				relation banned: user
  1160  				permission view = viewer - banned
  1161    		 }`,
  1162  			(func() []*corev1.RelationTuple {
  1163  				tuples := make([]*corev1.RelationTuple, 0, 201)
  1164  				tuples = append(tuples, tuple.MustParse("document:first#viewer@user:*"))
  1165  				for i := 0; i < 200; i++ {
  1166  					tuples = append(tuples, tuple.MustParse(fmt.Sprintf("document:first#banned@user:u%03d", i)))
  1167  				}
  1168  				return tuples
  1169  			})(),
  1170  			ONR("document", "first", "view"),
  1171  			RR("user", "..."),
  1172  			[]*v1.FoundSubject{
  1173  				{
  1174  					SubjectId: "*",
  1175  					ExcludedSubjects: (func() []*v1.FoundSubject {
  1176  						fs := make([]*v1.FoundSubject, 0, 200)
  1177  						for i := 0; i < 200; i++ {
  1178  							fs = append(fs, &v1.FoundSubject{SubjectId: fmt.Sprintf("u%03d", i)})
  1179  						}
  1180  						return fs
  1181  					})(),
  1182  				},
  1183  			},
  1184  		},
  1185  		{
  1186  			"simple arrow",
  1187  			[]int{0, 1, 2, 5, 100},
  1188  			`definition user {}
  1189  
  1190  	 		definition folder {
  1191  				relation parent: folder
  1192  				relation viewer: user
  1193  				permission view = viewer + parent->view	
  1194  			}
  1195  
  1196  		 	 definition document {
  1197  				relation parent: folder
  1198  				relation viewer: user
  1199  				permission view = viewer + parent->view
  1200    		    }`,
  1201  			[]*corev1.RelationTuple{
  1202  				tuple.MustParse("document:first#viewer@user:sarah"),
  1203  				tuple.MustParse("document:first#viewer@user:fred"),
  1204  
  1205  				tuple.MustParse("document:first#parent@folder:somefolder"),
  1206  				tuple.MustParse("folder:somefolder#viewer@user:victoria"),
  1207  				tuple.MustParse("folder:somefolder#viewer@user:tommy"),
  1208  
  1209  				tuple.MustParse("folder:somefolder#parent@folder:another"),
  1210  				tuple.MustParse("folder:another#viewer@user:diana"),
  1211  
  1212  				tuple.MustParse("folder:another#parent@folder:root"),
  1213  				tuple.MustParse("folder:root#viewer@user:zeus"),
  1214  			},
  1215  			ONR("document", "first", "view"),
  1216  			RR("user", "..."),
  1217  			[]*v1.FoundSubject{
  1218  				{SubjectId: "sarah"},
  1219  				{SubjectId: "fred"},
  1220  				{SubjectId: "victoria"},
  1221  				{SubjectId: "diana"},
  1222  				{SubjectId: "tommy"},
  1223  				{SubjectId: "zeus"},
  1224  			},
  1225  		},
  1226  		{
  1227  			"simple indirect",
  1228  			[]int{0, 1, 2, 5, 100},
  1229  			`definition user {}
  1230  
  1231  		 	 definition document {
  1232  				relation viewer: user | document#viewer
  1233  				permission view = viewer
  1234       		 }`,
  1235  			[]*corev1.RelationTuple{
  1236  				tuple.MustParse("document:first#viewer@user:sarah"),
  1237  				tuple.MustParse("document:first#viewer@user:fred"),
  1238  
  1239  				tuple.MustParse("document:second#viewer@user:tom"),
  1240  				tuple.MustParse("document:second#viewer@user:mark"),
  1241  
  1242  				tuple.MustParse("document:first#viewer@document:second#viewer"),
  1243  			},
  1244  			ONR("document", "first", "view"),
  1245  			RR("user", "..."),
  1246  			[]*v1.FoundSubject{
  1247  				{SubjectId: "sarah"},
  1248  				{SubjectId: "fred"},
  1249  				{SubjectId: "tom"},
  1250  				{SubjectId: "mark"},
  1251  			},
  1252  		},
  1253  		{
  1254  			"indirect with combined caveat",
  1255  			[]int{0, 1, 2, 5, 100},
  1256  			`definition user {}
  1257  
  1258  			 caveat somecaveat(some int) {
  1259  				some == 42
  1260  			 }
  1261  
  1262  			 caveat anothercaveat(some int) {
  1263  				some == 43
  1264  			 }
  1265  
  1266  			 definition otherresource {
  1267  		 	 	relation viewer: user with anothercaveat
  1268  		 	 }
  1269  
  1270  		 	 definition document {
  1271  				relation viewer: user with somecaveat | otherresource#viewer
  1272  				permission view = viewer
  1273       		 }`,
  1274  			[]*corev1.RelationTuple{
  1275  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"),
  1276  
  1277  				tuple.MustWithCaveat(tuple.MustParse("otherresource:second#viewer@user:tom"), "anothercaveat"),
  1278  
  1279  				tuple.MustParse("document:first#viewer@otherresource:second#viewer"),
  1280  			},
  1281  			ONR("document", "first", "view"),
  1282  			RR("user", "..."),
  1283  			[]*v1.FoundSubject{
  1284  				{
  1285  					SubjectId: "tom",
  1286  					CaveatExpression: caveatOr(
  1287  						caveatexpr("somecaveat"),
  1288  						caveatexpr("anothercaveat"),
  1289  					),
  1290  				},
  1291  			},
  1292  		},
  1293  		{
  1294  			"indirect with combined caveat direct",
  1295  			[]int{0, 1, 2, 5, 100},
  1296  			`definition user {}
  1297  
  1298  			 caveat somecaveat(some int) {
  1299  				some == 42
  1300  			 }
  1301  
  1302  			 caveat anothercaveat(some int) {
  1303  				some == 43
  1304  			 }
  1305  
  1306  			 definition otherresource {
  1307  		 	 	relation viewer: user with anothercaveat
  1308  		 	 }
  1309  
  1310  		 	 definition document {
  1311  				relation viewer: user with somecaveat | otherresource#viewer
  1312  				permission view = viewer
  1313       		 }`,
  1314  			[]*corev1.RelationTuple{
  1315  				tuple.MustWithCaveat(tuple.MustParse("document:first#viewer@user:tom"), "somecaveat"),
  1316  
  1317  				tuple.MustWithCaveat(tuple.MustParse("otherresource:second#viewer@user:tom"), "anothercaveat"),
  1318  
  1319  				tuple.MustParse("document:first#viewer@otherresource:second#viewer"),
  1320  			},
  1321  			ONR("document", "first", "viewer"),
  1322  			RR("user", "..."),
  1323  			[]*v1.FoundSubject{
  1324  				{
  1325  					SubjectId: "tom",
  1326  					CaveatExpression: caveatOr(
  1327  						caveatexpr("somecaveat"),
  1328  						caveatexpr("anothercaveat"),
  1329  					),
  1330  				},
  1331  			},
  1332  		},
  1333  		{
  1334  			"non-terminal subject",
  1335  			[]int{0, 1, 2, 5, 100},
  1336  			`definition user {}
  1337  
  1338  		 	 definition document {
  1339  				relation viewer: user | document#viewer
  1340  				permission view = viewer
  1341       		 }`,
  1342  			[]*corev1.RelationTuple{
  1343  				tuple.MustParse("document:first#viewer@user:sarah"),
  1344  				tuple.MustParse("document:first#viewer@user:fred"),
  1345  
  1346  				tuple.MustParse("document:second#viewer@user:tom"),
  1347  				tuple.MustParse("document:second#viewer@user:mark"),
  1348  
  1349  				tuple.MustParse("document:first#viewer@document:second#viewer"),
  1350  			},
  1351  			ONR("document", "first", "view"),
  1352  			RR("document", "viewer"),
  1353  			[]*v1.FoundSubject{
  1354  				{SubjectId: "first"},
  1355  				{SubjectId: "second"},
  1356  			},
  1357  		},
  1358  		{
  1359  			"indirect non-terminal subject",
  1360  			[]int{0, 1, 2, 5, 100},
  1361  			`definition user {}
  1362  
  1363     		     definition folder {
  1364  				relation parent_view: folder#view
  1365  				relation viewer: user
  1366  				permission view = viewer + parent_view
  1367  			 }
  1368  
  1369  			 definition document {
  1370  			   relation parent_view: folder#view
  1371  			   relation viewer: user
  1372  			   permission view = viewer + parent_view
  1373  			 }`,
  1374  			[]*corev1.RelationTuple{
  1375  				tuple.MustParse("document:first#parent_view@folder:somefolder#view"),
  1376  				tuple.MustParse("folder:somefolder#parent_view@folder:anotherfolder#view"),
  1377  			},
  1378  			ONR("document", "first", "view"),
  1379  			RR("folder", "view"),
  1380  			[]*v1.FoundSubject{
  1381  				{SubjectId: "anotherfolder"},
  1382  				{SubjectId: "somefolder"},
  1383  			},
  1384  		},
  1385  		{
  1386  			"large direct",
  1387  			[]int{0, 100, 104, 503, 1012, 10056},
  1388  			`definition user {}
  1389  
  1390  		 	 definition document {
  1391  				relation viewer: user
  1392  				permission view = viewer
  1393    		 }`,
  1394  			(func() []*corev1.RelationTuple {
  1395  				tuples := make([]*corev1.RelationTuple, 0, 20000)
  1396  				for i := 0; i < 20000; i++ {
  1397  					tuples = append(tuples, tuple.MustParse(fmt.Sprintf("document:first#viewer@user:u%03d", i)))
  1398  				}
  1399  				return tuples
  1400  			})(),
  1401  			ONR("document", "first", "view"),
  1402  			RR("user", "..."),
  1403  			(func() []*v1.FoundSubject {
  1404  				fs := make([]*v1.FoundSubject, 0, 20000)
  1405  				for i := 0; i < 20000; i++ {
  1406  					fs = append(fs, &v1.FoundSubject{SubjectId: fmt.Sprintf("u%03d", i)})
  1407  				}
  1408  				return fs
  1409  			})(),
  1410  		},
  1411  		{
  1412  			"large with intersection",
  1413  			[]int{0, 100, 104, 503, 1012, 10056},
  1414  			`definition user {}
  1415  
  1416  		 	 definition document {
  1417  				relation viewer1: user
  1418  				relation viewer2: user
  1419  				permission view = viewer1 & viewer2
  1420    		 }`,
  1421  			(func() []*corev1.RelationTuple {
  1422  				tuples := make([]*corev1.RelationTuple, 0, 20000)
  1423  				for i := 0; i < 20000; i++ {
  1424  					tuples = append(tuples, tuple.MustParse(fmt.Sprintf("document:first#viewer1@user:u%03d", i)))
  1425  					tuples = append(tuples, tuple.MustParse(fmt.Sprintf("document:first#viewer2@user:u%03d", i)))
  1426  				}
  1427  				return tuples
  1428  			})(),
  1429  			ONR("document", "first", "view"),
  1430  			RR("user", "..."),
  1431  			(func() []*v1.FoundSubject {
  1432  				fs := make([]*v1.FoundSubject, 0, 20000)
  1433  				for i := 0; i < 20000; i++ {
  1434  					fs = append(fs, &v1.FoundSubject{SubjectId: fmt.Sprintf("u%03d", i)})
  1435  				}
  1436  				return fs
  1437  			})(),
  1438  		},
  1439  		{
  1440  			"large with partial intersection",
  1441  			[]int{0, 100, 104, 503, 1012, 10056},
  1442  			`definition user {}
  1443  
  1444  		 	 definition document {
  1445  				relation viewer1: user
  1446  				relation viewer2: user
  1447  				permission view = viewer1 & viewer2
  1448    		 }`,
  1449  			(func() []*corev1.RelationTuple {
  1450  				tuples := make([]*corev1.RelationTuple, 0, 20000)
  1451  				for i := 0; i < 20000; i++ {
  1452  					tuples = append(tuples, tuple.MustParse(fmt.Sprintf("document:first#viewer1@user:u%03d", i)))
  1453  
  1454  					if i >= 10000 {
  1455  						tuples = append(tuples, tuple.MustParse(fmt.Sprintf("document:first#viewer2@user:u%03d", i)))
  1456  					}
  1457  				}
  1458  				return tuples
  1459  			})(),
  1460  			ONR("document", "first", "view"),
  1461  			RR("user", "..."),
  1462  			(func() []*v1.FoundSubject {
  1463  				fs := make([]*v1.FoundSubject, 0, 10000)
  1464  				for i := 10000; i < 20000; i++ {
  1465  					fs = append(fs, &v1.FoundSubject{SubjectId: fmt.Sprintf("u%03d", i)})
  1466  				}
  1467  				return fs
  1468  			})(),
  1469  		},
  1470  	}
  1471  
  1472  	for _, tc := range testCases {
  1473  		tc := tc
  1474  		t.Run(tc.name, func(t *testing.T) {
  1475  			for _, limit := range tc.pageSizes {
  1476  				t.Run(fmt.Sprintf("limit-%d_", limit), func(t *testing.T) {
  1477  					require := require.New(t)
  1478  
  1479  					dispatcher := NewLocalOnlyDispatcher(10)
  1480  
  1481  					ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
  1482  					require.NoError(err)
  1483  
  1484  					ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships(ds, tc.schema, tc.relationships, require)
  1485  
  1486  					ctx := datastoremw.ContextWithHandle(context.Background())
  1487  					require.NoError(datastoremw.SetInContext(ctx, ds))
  1488  
  1489  					var cursor *v1.Cursor
  1490  					overallResults := []*v1.FoundSubject{}
  1491  
  1492  					iterCount := 1
  1493  					if limit > 0 {
  1494  						iterCount = (len(tc.expected) / limit) + 1
  1495  					}
  1496  
  1497  					for i := 0; i < iterCount; i++ {
  1498  						stream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupSubjectsResponse](ctx)
  1499  						err = dispatcher.DispatchLookupSubjects(&v1.DispatchLookupSubjectsRequest{
  1500  							ResourceRelation: &corev1.RelationReference{
  1501  								Namespace: tc.start.Namespace,
  1502  								Relation:  tc.start.Relation,
  1503  							},
  1504  							ResourceIds:     []string{tc.start.ObjectId},
  1505  							SubjectRelation: tc.target,
  1506  							Metadata: &v1.ResolverMeta{
  1507  								AtRevision:     revision.String(),
  1508  								DepthRemaining: 50,
  1509  							},
  1510  							OptionalLimit:  uint32(limit),
  1511  							OptionalCursor: cursor,
  1512  						}, stream)
  1513  						require.NoError(err)
  1514  
  1515  						results := []*v1.FoundSubject{}
  1516  						hasWildcard := false
  1517  
  1518  						for _, streamResult := range stream.Results() {
  1519  							for _, foundSubjects := range streamResult.FoundSubjectsByResourceId {
  1520  								results = append(results, foundSubjects.FoundSubjects...)
  1521  								for _, fs := range foundSubjects.FoundSubjects {
  1522  									if fs.SubjectId == tuple.PublicWildcard {
  1523  										hasWildcard = true
  1524  									}
  1525  								}
  1526  							}
  1527  							cursor = streamResult.AfterResponseCursor
  1528  						}
  1529  
  1530  						if limit > 0 {
  1531  							// If there is a wildcard, its allowed to bypass the limit.
  1532  							if hasWildcard {
  1533  								require.LessOrEqual(len(results), limit+1)
  1534  							} else {
  1535  								require.LessOrEqual(len(results), limit)
  1536  							}
  1537  						}
  1538  
  1539  						overallResults = append(overallResults, results...)
  1540  					}
  1541  
  1542  					// NOTE: since cursored LS now can return a wildcard multiple times, we need to combine
  1543  					// them here before comparison.
  1544  					normalizedResults := combineWildcards(overallResults)
  1545  					itestutil.RequireEquivalentSets(t, tc.expected, normalizedResults)
  1546  				})
  1547  			}
  1548  		})
  1549  	}
  1550  }
  1551  
  1552  func combineWildcards(results []*v1.FoundSubject) []*v1.FoundSubject {
  1553  	combined := make([]*v1.FoundSubject, 0, len(results))
  1554  	var wildcardResult *v1.FoundSubject
  1555  	for _, result := range results {
  1556  		if result.SubjectId != tuple.PublicWildcard {
  1557  			combined = append(combined, result)
  1558  			continue
  1559  		}
  1560  
  1561  		if wildcardResult == nil {
  1562  			wildcardResult = result
  1563  			combined = append(combined, result)
  1564  			continue
  1565  		}
  1566  
  1567  		wildcardResult.ExcludedSubjects = append(wildcardResult.ExcludedSubjects, result.ExcludedSubjects...)
  1568  	}
  1569  	return combined
  1570  }