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

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