github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/typesystem/reachabilitygraph_test.go (about)

     1  package typesystem
     2  
     3  import (
     4  	"context"
     5  	"sort"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/require"
     9  
    10  	"github.com/authzed/spicedb/internal/datastore/memdb"
    11  	datastoremw "github.com/authzed/spicedb/internal/middleware/datastore"
    12  	"github.com/authzed/spicedb/pkg/datastore"
    13  	ns "github.com/authzed/spicedb/pkg/namespace"
    14  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    15  	"github.com/authzed/spicedb/pkg/schemadsl/compiler"
    16  	"github.com/authzed/spicedb/pkg/schemadsl/input"
    17  	"github.com/authzed/spicedb/pkg/tuple"
    18  )
    19  
    20  func TestRelationsEncounteredForSubject(t *testing.T) {
    21  	tcs := []struct {
    22  		name                string
    23  		schema              string
    24  		subjectType         string
    25  		relation            string
    26  		expectedPermissions []string
    27  	}{
    28  		{
    29  			"simple permission",
    30  			`definition user {}
    31  
    32  			definition document {
    33  				relation viewer: user
    34  				permission view = viewer
    35  			}`,
    36  
    37  			"document",
    38  			"viewer",
    39  			[]string{"document#view"},
    40  		},
    41  		{
    42  			"simple permission and user",
    43  			`definition user {}
    44  
    45  			definition document {
    46  				relation viewer: user
    47  				permission view = viewer
    48  			}`,
    49  
    50  			"user",
    51  			"...",
    52  			[]string{"document#view", "document#viewer"},
    53  		},
    54  		{
    55  			"multiple permissions using the same relation",
    56  			`definition user {}
    57  
    58  			definition document {
    59  				relation viewer: user
    60  				relation editor: user
    61  
    62  				permission edit = editor
    63  				permission view = viewer + editor
    64  			}`,
    65  
    66  			"document",
    67  			"editor",
    68  			[]string{"document#view", "document#edit"},
    69  		},
    70  		{
    71  			"multiple permissions using the same relation indirectly",
    72  			`definition user {}
    73  
    74  			definition document {
    75  				relation viewer: user
    76  				relation editor: user
    77  
    78  				permission edit = editor
    79  				permission view = viewer + edit
    80  			}`,
    81  
    82  			"document",
    83  			"editor",
    84  			[]string{"document#view", "document#edit"},
    85  		},
    86  		{
    87  			"permission referencing subject relation",
    88  			`definition user {}
    89  
    90  			definition group {
    91  				relation member: user
    92  			}
    93  
    94  			definition document {
    95  				relation viewer: user | group#member
    96  				permission view = viewer
    97  			}`,
    98  			"group",
    99  			"member",
   100  			[]string{"document#view", "document#viewer"},
   101  		},
   102  		{
   103  			"simple arrow",
   104  			`definition user {}
   105  
   106  			definition organization {
   107  				relation admin: user
   108  			}
   109  
   110  			definition document {
   111  				relation org: organization
   112  				permission view = org->admin
   113  			}`,
   114  			"organization",
   115  			"admin",
   116  			[]string{"document#view"},
   117  		},
   118  		{
   119  			"complex schema",
   120  			`definition user {}
   121  
   122  			definition organization {
   123  				relation admin: user
   124  				relation direct_member: user
   125  
   126  				permission member = direct_member + admin
   127  			}
   128  
   129  			definition group {
   130  				relation admin: user
   131  				relation direct_member: user | group#member
   132  				permission member = direct_member + admin
   133  			}
   134  
   135  			definition document {
   136  				relation viewer: user | group#member
   137  				relation owner: user
   138  				relation org: organization
   139  
   140  				permission view = viewer + owner + org->admin
   141  			}`,
   142  			"user",
   143  			"...",
   144  			[]string{
   145  				"document#viewer", "document#owner", "document#view",
   146  				"group#admin", "group#direct_member", "group#member",
   147  				"organization#member", "organization#admin",
   148  				"organization#direct_member",
   149  			},
   150  		},
   151  		{
   152  			"schema with different user types",
   153  			`definition user {}
   154  
   155  			definition platformuser {}
   156  
   157  			definition document {
   158  				relation viewer: user | platformuser
   159  				relation editor: platformuser
   160  
   161  				permission edit = editor
   162  				permission view = viewer + edit
   163  			}`,
   164  
   165  			"user",
   166  			"...",
   167  
   168  			[]string{"document#view", "document#viewer"},
   169  		},
   170  	}
   171  
   172  	for _, tc := range tcs {
   173  		tc := tc
   174  		t.Run(tc.name, func(t *testing.T) {
   175  			require := require.New(t)
   176  
   177  			ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   178  			require.NoError(err)
   179  
   180  			ctx := datastoremw.ContextWithDatastore(context.Background(), ds)
   181  
   182  			compiled, err := compiler.Compile(compiler.InputSchema{
   183  				Source:       input.Source("schema"),
   184  				SchemaString: tc.schema,
   185  			}, compiler.AllowUnprefixedObjectType())
   186  			require.NoError(err)
   187  
   188  			// Write the schema.
   189  			_, err = ds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error {
   190  				for _, nsDef := range compiled.ObjectDefinitions {
   191  					if err := tx.WriteNamespaces(ctx, nsDef); err != nil {
   192  						return err
   193  					}
   194  				}
   195  
   196  				return nil
   197  			})
   198  			require.NoError(err)
   199  
   200  			lastRevision, err := ds.HeadRevision(context.Background())
   201  			require.NoError(err)
   202  
   203  			reader := ds.SnapshotReader(lastRevision)
   204  
   205  			_, vts, err := ReadNamespaceAndTypes(ctx, tc.subjectType, reader)
   206  			require.NoError(err)
   207  
   208  			rg := ReachabilityGraphFor(vts)
   209  
   210  			relations, err := rg.RelationsEncounteredForSubject(ctx, compiled.ObjectDefinitions, &core.RelationReference{
   211  				Namespace: tc.subjectType,
   212  				Relation:  tc.relation,
   213  			})
   214  			require.NoError(err)
   215  
   216  			relationStrs := make([]string, 0, len(relations))
   217  			for _, relation := range relations {
   218  				relationStrs = append(relationStrs, tuple.StringRR(relation))
   219  			}
   220  
   221  			sort.Strings(relationStrs)
   222  			sort.Strings(tc.expectedPermissions)
   223  
   224  			require.Equal(tc.expectedPermissions, relationStrs)
   225  		})
   226  	}
   227  }
   228  
   229  func TestRelationsEncounteredForResource(t *testing.T) {
   230  	tcs := []struct {
   231  		name              string
   232  		schema            string
   233  		resourceType      string
   234  		permission        string
   235  		expectedRelations []string
   236  	}{
   237  		{
   238  			"simple relation",
   239  			`definition user {}
   240  
   241  			definition document {
   242  				relation viewer: user
   243  			}`,
   244  
   245  			"document",
   246  			"viewer",
   247  			[]string{"document#viewer"},
   248  		},
   249  		{
   250  			"simple permission",
   251  			`definition user {}
   252  
   253  			definition document {
   254  				relation viewer: user
   255  				permission view = viewer + nil
   256  			}`,
   257  			"document",
   258  			"view",
   259  			[]string{"document#viewer", "document#view"},
   260  		},
   261  		{
   262  			"permission with multiple relations",
   263  			`definition user {}
   264  
   265  			definition document {
   266  				relation viewer: user
   267  				relation editor: user
   268  				relation owner: user
   269  				permission view = viewer + editor + owner
   270  			}`,
   271  			"document",
   272  			"view",
   273  			[]string{"document#editor", "document#owner", "document#viewer", "document#view"},
   274  		},
   275  		{
   276  			"permission with multiple relations under intersection",
   277  			`definition user {}
   278  
   279  			definition document {
   280  				relation viewer: user
   281  				relation editor: user
   282  				relation owner: user
   283  
   284  				permission view = viewer & editor & owner
   285  			}`,
   286  			"document",
   287  			"view",
   288  			[]string{"document#viewer", "document#view", "document#editor", "document#owner"},
   289  		},
   290  		{
   291  			"permission with multiple relations under exclusion",
   292  			`definition user {}
   293  
   294  			definition document {
   295  				relation viewer: user
   296  				relation editor: user
   297  				relation owner: user
   298  
   299  				permission view = viewer - editor - owner
   300  			}`,
   301  			"document",
   302  			"view",
   303  			[]string{"document#viewer", "document#view", "document#editor", "document#owner"},
   304  		},
   305  		{
   306  			"permission with arrow",
   307  			`definition user {}
   308  
   309  			definition organization {
   310  				relation admin: user
   311  			}
   312  
   313  			definition document {
   314  				relation org: organization
   315  				relation viewer: user
   316  				relation owner: user
   317  
   318  				permission view = viewer + owner + org->admin
   319  			}`,
   320  			"document",
   321  			"view",
   322  			[]string{"document#viewer", "document#owner", "document#org", "document#view", "organization#admin"},
   323  		},
   324  		{
   325  			"permission with subrelation",
   326  			`definition user {}
   327  
   328  			definition group {
   329  				relation direct_member: user
   330  				relation admin: user
   331  
   332  				permission member = direct_member + admin
   333  			}
   334  
   335  			definition document {
   336  				relation viewer: user | group#member
   337  
   338  				permission view = viewer
   339  			}`,
   340  
   341  			"document",
   342  			"view",
   343  
   344  			[]string{"document#viewer", "document#view", "group#direct_member", "group#admin", "group#member"},
   345  		},
   346  		{
   347  			"permission with unused relation",
   348  			`definition user {}
   349  
   350  			definition resource {
   351  				relation viewer: user
   352  				relation editor: user
   353  				relation owner: user
   354  				relation unused: user
   355  
   356  				permission view = viewer + editor + owner
   357  			}`,
   358  
   359  			"resource",
   360  			"view",
   361  
   362  			[]string{"resource#viewer", "resource#editor", "resource#owner", "resource#view"},
   363  		},
   364  		{
   365  			"permission with multiple arrows",
   366  			`definition user {}
   367  
   368  			definition organization {
   369  				relation admin: user
   370  				relation banned: user
   371  				relation member: user
   372  
   373  				permission can_admin = admin - banned
   374  			}
   375  
   376  			definition group {
   377  				relation admin: user
   378  				relation direct_member: user | group#member
   379  				relation org: organization
   380  
   381  				permission member = direct_member + admin + org->can_admin
   382  			}
   383  
   384  			definition document {
   385  				relation viewer: user | group#member
   386  				relation owner: user
   387  				relation org: organization
   388  
   389  				permission view = viewer + owner + org->member
   390  			}`,
   391  
   392  			"document",
   393  			"view",
   394  
   395  			[]string{
   396  				"document#viewer", "document#owner", "document#org", "document#view",
   397  				"group#admin", "group#direct_member", "group#member", "organization#admin",
   398  				"organization#member", "organization#can_admin", "group#org", "organization#banned",
   399  			},
   400  		},
   401  		{
   402  			"permission with multiple arrows but only one used",
   403  			`definition user {}
   404  
   405  			definition organization {
   406  				relation admin: user
   407  				relation banned: user
   408  				relation member: user
   409  
   410  				permission can_admin = admin - banned
   411  			}
   412  
   413  			definition group {
   414  				relation admin: user
   415  				relation direct_member: user | group#member
   416  				relation org: organization
   417  
   418  				permission member = direct_member + admin + org->can_admin
   419  			}
   420  
   421  			definition document {
   422  				relation viewer: user | group#member
   423  				relation owner: user
   424  				relation org: organization
   425  
   426  				permission view = viewer + owner
   427  			}`,
   428  
   429  			"document",
   430  			"view",
   431  
   432  			[]string{
   433  				"document#viewer", "document#owner", "document#view",
   434  				"group#admin", "group#direct_member", "group#member", "organization#admin",
   435  				"organization#can_admin", "group#org", "organization#banned",
   436  			},
   437  		},
   438  		{
   439  			"permission with multiple items but only one path used",
   440  			`definition user {}
   441  
   442  			definition organization {
   443  				relation admin: user
   444  				relation banned: user
   445  				relation member: user
   446  
   447  				permission can_admin = admin - banned
   448  			}
   449  
   450  			definition group {
   451  				relation admin: user
   452  				relation direct_member: user | group#member
   453  				relation org: organization
   454  
   455  				permission member = direct_member + admin + org->can_admin
   456  			}
   457  
   458  			definition document {
   459  				relation viewer: user
   460  				relation owner: user
   461  				relation org: organization
   462  
   463  				permission view = viewer + owner
   464  			}`,
   465  
   466  			"document",
   467  			"view",
   468  
   469  			[]string{
   470  				"document#viewer", "document#owner", "document#view",
   471  			},
   472  		},
   473  		{
   474  			"permission with many indirect relations",
   475  			`definition user {}
   476  
   477  			definition first {
   478  				relation member: second#member
   479  			}
   480  
   481  			definition second {
   482  				relation member: third#member
   483  			}
   484  
   485  			definition third {
   486  				relation member: user
   487  			}
   488  
   489  			definition document {
   490  				relation viewer: user | first#member
   491  				permission view = viewer
   492  			}`,
   493  
   494  			"document",
   495  			"view",
   496  
   497  			[]string{
   498  				"document#viewer", "document#view", "first#member", "second#member", "third#member",
   499  			},
   500  		},
   501  	}
   502  
   503  	for _, tc := range tcs {
   504  		tc := tc
   505  		t.Run(tc.name, func(t *testing.T) {
   506  			require := require.New(t)
   507  
   508  			ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   509  			require.NoError(err)
   510  
   511  			ctx := datastoremw.ContextWithDatastore(context.Background(), ds)
   512  
   513  			compiled, err := compiler.Compile(compiler.InputSchema{
   514  				Source:       input.Source("schema"),
   515  				SchemaString: tc.schema,
   516  			}, compiler.AllowUnprefixedObjectType())
   517  			require.NoError(err)
   518  
   519  			// Write the schema.
   520  			_, err = ds.ReadWriteTx(context.Background(), func(ctx context.Context, tx datastore.ReadWriteTransaction) error {
   521  				for _, nsDef := range compiled.ObjectDefinitions {
   522  					if err := tx.WriteNamespaces(ctx, nsDef); err != nil {
   523  						return err
   524  					}
   525  				}
   526  
   527  				return nil
   528  			})
   529  			require.NoError(err)
   530  
   531  			lastRevision, err := ds.HeadRevision(context.Background())
   532  			require.NoError(err)
   533  
   534  			reader := ds.SnapshotReader(lastRevision)
   535  
   536  			_, vts, err := ReadNamespaceAndTypes(ctx, tc.resourceType, reader)
   537  			require.NoError(err)
   538  
   539  			rg := ReachabilityGraphFor(vts)
   540  
   541  			relations, err := rg.RelationsEncounteredForResource(ctx, &core.RelationReference{
   542  				Namespace: tc.resourceType,
   543  				Relation:  tc.permission,
   544  			})
   545  			require.NoError(err)
   546  
   547  			relationStrs := make([]string, 0, len(relations))
   548  			for _, relation := range relations {
   549  				relationStrs = append(relationStrs, tuple.StringRR(relation))
   550  			}
   551  
   552  			sort.Strings(relationStrs)
   553  			sort.Strings(tc.expectedRelations)
   554  
   555  			require.Equal(tc.expectedRelations, relationStrs)
   556  		})
   557  	}
   558  }
   559  
   560  func TestReachabilityGraph(t *testing.T) {
   561  	testCases := []struct {
   562  		name                                 string
   563  		schema                               string
   564  		resourceType                         *core.RelationReference
   565  		subjectType                          *core.RelationReference
   566  		expectedFullEntrypointRelations      []rrtStruct
   567  		expectedOptimizedEntrypointRelations []rrtStruct
   568  	}{
   569  		{
   570  			"single relation",
   571  			`definition user {}
   572  
   573  			definition document {
   574  				relation viewer: user
   575  			}`,
   576  			rr("document", "viewer"),
   577  			rr("user", "..."),
   578  			[]rrtStruct{rrt("document", "viewer", true)},
   579  			[]rrtStruct{rrt("document", "viewer", true)},
   580  		},
   581  		{
   582  			"simple permission",
   583  			`definition user {}
   584  
   585  			definition document {
   586  				relation viewer: user
   587  				permission view = viewer + nil
   588  			}`,
   589  			rr("document", "view"),
   590  			rr("user", "..."),
   591  			[]rrtStruct{rrt("document", "viewer", true)},
   592  			[]rrtStruct{rrt("document", "viewer", true)},
   593  		},
   594  		{
   595  			"permission with multiple relations",
   596  			`definition user {}
   597  
   598  			definition document {
   599  				relation viewer: user
   600  				relation editor: user
   601  				relation owner: user
   602  				permission view = viewer + editor + owner
   603  			}`,
   604  			rr("document", "view"),
   605  			rr("user", "..."),
   606  			[]rrtStruct{
   607  				rrt("document", "editor", true),
   608  				rrt("document", "owner", true),
   609  				rrt("document", "viewer", true),
   610  			},
   611  			[]rrtStruct{
   612  				rrt("document", "editor", true),
   613  				rrt("document", "owner", true),
   614  				rrt("document", "viewer", true),
   615  			},
   616  		},
   617  		{
   618  			"permission with multiple relations under intersection",
   619  			`definition user {}
   620  
   621  			definition document {
   622  				relation viewer: user
   623  				relation editor: user
   624  				relation owner: user
   625  				permission view = viewer & editor & owner
   626  			}`,
   627  			rr("document", "view"),
   628  			rr("user", "..."),
   629  			[]rrtStruct{
   630  				rrt("document", "editor", true),
   631  				rrt("document", "owner", true),
   632  				rrt("document", "viewer", true),
   633  			},
   634  			[]rrtStruct{
   635  				rrt("document", "viewer", true),
   636  			},
   637  		},
   638  		{
   639  			"permission with multiple relations under exclusion",
   640  			`definition user {}
   641  
   642  			definition document {
   643  				relation viewer: user
   644  				relation editor: user
   645  				relation owner: user
   646  				permission view = viewer - editor - owner
   647  			}`,
   648  			rr("document", "view"),
   649  			rr("user", "..."),
   650  			[]rrtStruct{
   651  				rrt("document", "editor", true),
   652  				rrt("document", "owner", true),
   653  				rrt("document", "viewer", true),
   654  			},
   655  			[]rrtStruct{
   656  				rrt("document", "viewer", true),
   657  			},
   658  		},
   659  		{
   660  			"permission with arrow",
   661  			`definition user {}
   662  
   663  			definition organization {
   664  				relation admin: user
   665  			}
   666  
   667  			definition document {
   668  				relation org: organization
   669  				relation viewer: user
   670  				relation owner: user
   671  				permission view = viewer + owner + org->admin
   672  			}`,
   673  			rr("document", "view"),
   674  			rr("user", "..."),
   675  			[]rrtStruct{
   676  				rrt("document", "owner", true),
   677  				rrt("document", "viewer", true),
   678  				rrt("organization", "admin", true),
   679  			},
   680  			[]rrtStruct{
   681  				rrt("document", "owner", true),
   682  				rrt("document", "viewer", true),
   683  				rrt("organization", "admin", true),
   684  			},
   685  		},
   686  		{
   687  			"permission with multi-level arrows",
   688  			`definition user {}
   689  
   690  			definition organization {
   691  				relation admin: user
   692  			}
   693  
   694  			definition container {
   695  				relation parent: organization | container
   696  				permission admin = parent->admin
   697  			}
   698  
   699  			definition document {
   700  				relation container: container
   701  				relation viewer: user
   702  				relation owner: user
   703  				permission view = viewer + owner + container->admin
   704  			}`,
   705  			rr("document", "view"),
   706  			rr("user", "..."),
   707  			[]rrtStruct{
   708  				rrt("document", "owner", true),
   709  				rrt("document", "viewer", true),
   710  				rrt("organization", "admin", true),
   711  			},
   712  			[]rrtStruct{
   713  				rrt("document", "owner", true),
   714  				rrt("document", "viewer", true),
   715  				rrt("organization", "admin", true),
   716  			},
   717  		},
   718  		{
   719  			"permission with multi-level arrows and container",
   720  			`definition user {}
   721  
   722  			definition organization {
   723  				relation admin: user
   724  			}
   725  
   726  			definition container {
   727  				relation parent: organization | container
   728  				relation localadmin: user
   729  				permission admin = parent->admin + localadmin
   730  			}
   731  
   732  			definition document {
   733  				relation container: container
   734  				relation viewer: user
   735  				relation owner: user
   736  				permission view = viewer + owner + container->admin
   737  			}`,
   738  			rr("document", "view"),
   739  			rr("user", "..."),
   740  			[]rrtStruct{
   741  				rrt("container", "localadmin", true),
   742  				rrt("document", "owner", true),
   743  				rrt("document", "viewer", true),
   744  				rrt("organization", "admin", true),
   745  			},
   746  			[]rrtStruct{
   747  				rrt("container", "localadmin", true),
   748  				rrt("document", "owner", true),
   749  				rrt("document", "viewer", true),
   750  				rrt("organization", "admin", true),
   751  			},
   752  		},
   753  		{
   754  			"recursive relation",
   755  			`definition user {}
   756  
   757  			definition group {
   758  				relation direct_member: group#member | user
   759  				relation manager: group#member | user
   760  				permission member = direct_member + manager
   761  			}
   762  
   763  			definition document {
   764  				relation viewer: user | group#member
   765  				permission view = viewer
   766  			}`,
   767  			rr("document", "view"),
   768  			rr("user", "..."),
   769  			[]rrtStruct{
   770  				rrt("document", "viewer", true),
   771  				rrt("group", "direct_member", true),
   772  				rrt("group", "manager", true),
   773  			},
   774  			[]rrtStruct{
   775  				rrt("document", "viewer", true),
   776  				rrt("group", "direct_member", true),
   777  				rrt("group", "manager", true),
   778  			},
   779  		},
   780  		{
   781  			"arrow under exclusion",
   782  			`definition user {}
   783  
   784  			definition organization {
   785  				relation admin: user
   786  				relation banned: user
   787  			}
   788  
   789  			definition document {
   790  				relation org: organization
   791  				relation viewer: user
   792  				permission view = (viewer - org->banned) + org->admin
   793  			}`,
   794  			rr("document", "view"),
   795  			rr("user", "..."),
   796  			[]rrtStruct{
   797  				rrt("document", "viewer", true),
   798  				rrt("organization", "admin", true),
   799  				rrt("organization", "banned", true),
   800  			},
   801  			[]rrtStruct{
   802  				rrt("document", "viewer", true),
   803  				rrt("organization", "admin", true),
   804  			},
   805  		},
   806  		{
   807  			"multiple relations with different subject types",
   808  			`definition platform1user {}
   809  			definition platform2user {}
   810  
   811  			definition document {
   812  				relation viewer: platform1user | platform2user
   813  				relation editor: platform1user
   814  				relation owner: platform2user
   815  
   816  				permission view = viewer + editor + owner
   817  			}`,
   818  			rr("document", "view"),
   819  			rr("platform1user", "..."),
   820  			[]rrtStruct{
   821  				rrt("document", "editor", true),
   822  				rrt("document", "viewer", true),
   823  			},
   824  			[]rrtStruct{
   825  				rrt("document", "editor", true),
   826  				rrt("document", "viewer", true),
   827  			},
   828  		},
   829  		{
   830  			"optimized reachability",
   831  			`definition user {}
   832  
   833  			definition organization {
   834  				relation admin: user
   835  				relation banned: user
   836  			}
   837  
   838  			definition document {
   839  				relation org: organization
   840  				relation viewer: user
   841  				relation anotherrel: user
   842  				relation thirdrel: user
   843  				relation fourthrel: user
   844  				permission view = ((((viewer - org->banned) & org->admin) + anotherrel) - thirdrel) + fourthrel
   845  			}`,
   846  			rr("document", "view"),
   847  			rr("user", "..."),
   848  			[]rrtStruct{
   849  				rrt("document", "anotherrel", true),
   850  				rrt("document", "fourthrel", true),
   851  				rrt("document", "thirdrel", true),
   852  				rrt("document", "viewer", true),
   853  				rrt("organization", "admin", true),
   854  				rrt("organization", "banned", true),
   855  			},
   856  			[]rrtStruct{
   857  				rrt("document", "anotherrel", true),
   858  				rrt("document", "fourthrel", true),
   859  				rrt("document", "viewer", true),
   860  			},
   861  		},
   862  		{
   863  			"optimized reachability, within expression",
   864  			`definition user {}
   865  
   866  			definition organization {
   867  				relation admin: user
   868  				relation banned: user
   869  			}
   870  
   871  			definition document {
   872  				relation org: organization
   873  				relation viewer: user
   874  				relation anotherrel: user
   875  				relation thirdrel: user
   876  				relation fourthrel: user
   877  				permission view = ((((viewer - org->banned) & org->admin) + anotherrel) - thirdrel) + fourthrel
   878  			}`,
   879  			rr("document", "view"),
   880  			rr("document", "viewer"),
   881  			[]rrtStruct{
   882  				rrt("document", "view", false),
   883  			},
   884  			[]rrtStruct{
   885  				rrt("document", "view", false),
   886  			},
   887  		},
   888  		{
   889  			"optimized reachability, within expression 2",
   890  			`definition user {}
   891  
   892  			definition organization {
   893  				relation admin: user
   894  				relation banned: user
   895  			}
   896  
   897  			definition document {
   898  				relation org: organization
   899  				relation viewer: user
   900  				relation anotherrel: user
   901  				relation thirdrel: user
   902  				relation fourthrel: user
   903  				permission view = ((((viewer - org->banned) & org->admin) + anotherrel) - thirdrel) + fourthrel
   904  			}`,
   905  			rr("document", "view"),
   906  			rr("organization", "admin"),
   907  			[]rrtStruct{
   908  				rrt("document", "view", false),
   909  			},
   910  			[]rrtStruct{},
   911  		},
   912  		{
   913  			"intermediate reachability",
   914  			`definition user {}
   915  
   916  			definition organization {
   917  				relation admin: user
   918  			}
   919  
   920  			definition container {
   921  				relation parent: organization | container
   922  				relation localadmin: user
   923  				permission admin = parent->admin + localadmin
   924  				permission anotherthing = localadmin
   925  			}
   926  
   927  			definition document {
   928  				relation container: container
   929  				relation viewer: user
   930  				relation owner: user
   931  				permission view = viewer + owner + container->admin + container->anotherthing
   932  			}`,
   933  			rr("document", "view"),
   934  			rr("container", "localadmin"),
   935  			[]rrtStruct{
   936  				rrt("container", "admin", true),
   937  				rrt("container", "anotherthing", true),
   938  			},
   939  			[]rrtStruct{
   940  				rrt("container", "admin", true),
   941  				rrt("container", "anotherthing", true),
   942  			},
   943  		},
   944  		{
   945  			"intermediate reachability with intersection",
   946  			`definition user {}
   947  
   948  			definition organization {
   949  				relation admin: user
   950  			}
   951  
   952  			definition container {
   953  				relation parent: organization | container
   954  				relation localadmin: user
   955  				relation another: user
   956  				permission admin = parent->admin + localadmin
   957  				permission anotherthing = localadmin & another
   958  			}
   959  
   960  			definition document {
   961  				relation container: container
   962  				permission view = container->admin + container->anotherthing
   963  			}`,
   964  			rr("document", "view"),
   965  			rr("container", "localadmin"),
   966  			[]rrtStruct{
   967  				rrt("container", "admin", true),
   968  				rrt("container", "anotherthing", false),
   969  			},
   970  			[]rrtStruct{
   971  				rrt("container", "admin", true),
   972  				rrt("container", "anotherthing", false),
   973  			},
   974  		},
   975  		{
   976  			"relation reused",
   977  			`definition user {}
   978  
   979  			definition document {
   980  				relation viewer: user
   981  				relation another: user
   982  				permission view = viewer + viewer
   983  			}`,
   984  			rr("document", "view"),
   985  			rr("document", "viewer"),
   986  			[]rrtStruct{
   987  				rrt("document", "view", true),
   988  			},
   989  			[]rrtStruct{
   990  				rrt("document", "view", true),
   991  			},
   992  		},
   993  		{
   994  			"relation does not exist on one type of the arrow",
   995  			`definition user {}
   996  
   997  			definition team {}
   998  
   999  			definition organization {
  1000  				relation viewer: user
  1001  			}
  1002  
  1003  			definition document {
  1004  				relation parent: organization | team
  1005  				permission view = parent->viewer
  1006  			}`,
  1007  			rr("document", "view"),
  1008  			rr("user", "..."),
  1009  			[]rrtStruct{
  1010  				rrt("organization", "viewer", true),
  1011  			},
  1012  			[]rrtStruct{
  1013  				rrt("organization", "viewer", true),
  1014  			},
  1015  		},
  1016  	}
  1017  
  1018  	for _, tc := range testCases {
  1019  		tc := tc
  1020  		t.Run(tc.name, func(t *testing.T) {
  1021  			require := require.New(t)
  1022  
  1023  			ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
  1024  			require.NoError(err)
  1025  
  1026  			ctx := datastoremw.ContextWithDatastore(context.Background(), ds)
  1027  
  1028  			compiled, err := compiler.Compile(compiler.InputSchema{
  1029  				Source:       input.Source("schema"),
  1030  				SchemaString: tc.schema,
  1031  			}, compiler.AllowUnprefixedObjectType())
  1032  			require.NoError(err)
  1033  
  1034  			lastRevision, err := ds.HeadRevision(context.Background())
  1035  			require.NoError(err)
  1036  
  1037  			var rts *ValidatedNamespaceTypeSystem
  1038  			for _, nsDef := range compiled.ObjectDefinitions {
  1039  				reader := ds.SnapshotReader(lastRevision)
  1040  				ts, err := NewNamespaceTypeSystem(nsDef,
  1041  					ResolverForDatastoreReader(reader).WithPredefinedElements(PredefinedElements{
  1042  						Namespaces: compiled.ObjectDefinitions,
  1043  						Caveats:    compiled.CaveatDefinitions,
  1044  					}))
  1045  				require.NoError(err)
  1046  
  1047  				vts, terr := ts.Validate(ctx)
  1048  				require.NoError(terr)
  1049  
  1050  				if nsDef.Name == tc.resourceType.Namespace {
  1051  					rts = vts
  1052  				}
  1053  			}
  1054  			require.NotNil(rts)
  1055  
  1056  			foundEntrypoints, err := ReachabilityGraphFor(rts).AllEntrypointsForSubjectToResource(ctx, tc.subjectType, tc.resourceType)
  1057  			require.NoError(err)
  1058  			verifyEntrypoints(require, foundEntrypoints, tc.expectedFullEntrypointRelations)
  1059  
  1060  			foundOptEntrypoints, err := ReachabilityGraphFor(rts).OptimizedEntrypointsForSubjectToResource(ctx, tc.subjectType, tc.resourceType)
  1061  			require.NoError(err)
  1062  			verifyEntrypoints(require, foundOptEntrypoints, tc.expectedOptimizedEntrypointRelations)
  1063  		})
  1064  	}
  1065  }
  1066  
  1067  func verifyEntrypoints(require *require.Assertions, foundEntrypoints []ReachabilityEntrypoint, expectedEntrypoints []rrtStruct) {
  1068  	expectedEntrypointRelations := make([]string, 0, len(expectedEntrypoints))
  1069  	isDirectMap := map[string]bool{}
  1070  	for _, expected := range expectedEntrypoints {
  1071  		expectedEntrypointRelations = append(expectedEntrypointRelations, tuple.StringRR(expected.relationRef))
  1072  		isDirectMap[tuple.StringRR(expected.relationRef)] = expected.isDirect
  1073  	}
  1074  
  1075  	foundRelations := make([]string, 0, len(foundEntrypoints))
  1076  	for _, entrypoint := range foundEntrypoints {
  1077  		foundRelations = append(foundRelations, tuple.StringRR(entrypoint.ContainingRelationOrPermission()))
  1078  		if isDirect, ok := isDirectMap[tuple.StringRR(entrypoint.ContainingRelationOrPermission())]; ok {
  1079  			require.Equal(isDirect, entrypoint.IsDirectResult(), "found mismatch for whether a direct result for entrypoint for %s", entrypoint.parentRelation.Relation)
  1080  		}
  1081  	}
  1082  
  1083  	sort.Strings(expectedEntrypointRelations)
  1084  	sort.Strings(foundRelations)
  1085  	require.Equal(expectedEntrypointRelations, foundRelations)
  1086  }
  1087  
  1088  type rrtStruct struct {
  1089  	relationRef *core.RelationReference
  1090  	isDirect    bool
  1091  }
  1092  
  1093  func rr(namespace, relation string) *core.RelationReference {
  1094  	return ns.RelationReference(namespace, relation)
  1095  }
  1096  
  1097  func rrt(namespace, relation string, isDirect bool) rrtStruct {
  1098  	return rrtStruct{ns.RelationReference(namespace, relation), isDirect}
  1099  }