github.com/openfga/openfga@v1.5.4-rc1/pkg/server/test/reverse_expand.go (about)

     1  package test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/oklog/ulid/v2"
    10  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/openfga/openfga/internal/graph"
    14  	"github.com/openfga/openfga/pkg/server/commands/reverseexpand"
    15  	"github.com/openfga/openfga/pkg/storage"
    16  	storagetest "github.com/openfga/openfga/pkg/storage/test"
    17  	"github.com/openfga/openfga/pkg/tuple"
    18  	"github.com/openfga/openfga/pkg/typesystem"
    19  )
    20  
    21  func TestReverseExpand(t *testing.T, ds storage.OpenFGADatastore) {
    22  	tests := []struct {
    23  		name                 string
    24  		model                string
    25  		tuples               []string
    26  		request              *reverseexpand.ReverseExpandRequest
    27  		resolveNodeLimit     uint32
    28  		expectedResult       []*reverseexpand.ReverseExpandResult
    29  		expectedError        error
    30  		expectedDSQueryCount uint32
    31  	}{
    32  		{
    33  			name: "basic_intersection",
    34  			request: &reverseexpand.ReverseExpandRequest{
    35  				StoreID:    ulid.Make().String(),
    36  				ObjectType: "document",
    37  				Relation:   "viewer",
    38  				User: &reverseexpand.UserRefObject{
    39  					Object: &openfgav1.Object{
    40  						Type: "user",
    41  						Id:   "jon",
    42  					},
    43  				},
    44  				ContextualTuples: []*openfgav1.TupleKey{},
    45  			},
    46  			model: `
    47  			model
    48  			  schema 1.1
    49  
    50  			type user
    51  
    52  			type document
    53  			  relations
    54  			    define allowed: [user]
    55  			    define viewer: [user] and allowed`,
    56  			tuples: []string{
    57  				"document:1#viewer@user:jon",
    58  				"document:2#viewer@user:jon",
    59  				"document:3#allowed@user:jon",
    60  			},
    61  			expectedResult: []*reverseexpand.ReverseExpandResult{
    62  				{
    63  					Object:       "document:1",
    64  					ResultStatus: reverseexpand.RequiresFurtherEvalStatus,
    65  				},
    66  				{
    67  					Object:       "document:2",
    68  					ResultStatus: reverseexpand.RequiresFurtherEvalStatus,
    69  				},
    70  			},
    71  			expectedDSQueryCount: 1,
    72  		},
    73  		{
    74  			name: "indirect_intersection",
    75  			request: &reverseexpand.ReverseExpandRequest{
    76  				StoreID:    ulid.Make().String(),
    77  				ObjectType: "document",
    78  				Relation:   "viewer",
    79  				User: &reverseexpand.UserRefObject{
    80  					Object: &openfgav1.Object{
    81  						Type: "user",
    82  						Id:   "jon",
    83  					},
    84  				},
    85  				ContextualTuples: []*openfgav1.TupleKey{},
    86  			},
    87  			model: `
    88  			model
    89  			  schema 1.1
    90  
    91  			type user
    92  
    93  			type folder
    94  			  relations
    95  			    define writer: [user]
    96  			    define editor: [user]
    97  			    define viewer: writer and editor
    98  
    99  			type document
   100  			  relations
   101  			    define parent: [folder]
   102  			    define viewer: viewer from parent`,
   103  			tuples: []string{
   104  				"document:1#parent@folder:X",
   105  				"folder:X#writer@user:jon",
   106  				"folder:X#editor@user:jon",
   107  			},
   108  			expectedResult: []*reverseexpand.ReverseExpandResult{
   109  				{
   110  					Object:       "document:1",
   111  					ResultStatus: reverseexpand.RequiresFurtherEvalStatus,
   112  				},
   113  			},
   114  			expectedDSQueryCount: 2,
   115  		},
   116  
   117  		{
   118  			name: "resolve_direct_relationships_with_tuples_and_contextual_tuples",
   119  			request: &reverseexpand.ReverseExpandRequest{
   120  				StoreID:    ulid.Make().String(),
   121  				ObjectType: "document",
   122  				Relation:   "viewer",
   123  				User: &reverseexpand.UserRefObject{
   124  					Object: &openfgav1.Object{
   125  						Type: "user",
   126  						Id:   "jon",
   127  					},
   128  				},
   129  				ContextualTuples: []*openfgav1.TupleKey{
   130  					tuple.NewTupleKey("document:doc2", "viewer", "user:bob"),
   131  					tuple.NewTupleKey("document:doc3", "viewer", "user:jon"),
   132  				},
   133  			},
   134  			model: `
   135  			model
   136  			  schema 1.1
   137  
   138  			type user
   139  
   140  			type document
   141  			  relations
   142  			    define viewer: [user]`,
   143  			tuples: []string{
   144  				"document:doc1#viewer@user:jon",
   145  			},
   146  			expectedResult: []*reverseexpand.ReverseExpandResult{
   147  				{
   148  					Object:       "document:doc1",
   149  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   150  				},
   151  				{
   152  					Object:       "document:doc3",
   153  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   154  				},
   155  			},
   156  			expectedDSQueryCount: 1,
   157  		},
   158  		{
   159  			name: "direct_relations_involving_relationships_with_users_and_usersets",
   160  			request: &reverseexpand.ReverseExpandRequest{
   161  				StoreID:    ulid.Make().String(),
   162  				ObjectType: "document",
   163  				Relation:   "viewer",
   164  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
   165  					Type: "user",
   166  					Id:   "jon",
   167  				}},
   168  			},
   169  			model: `
   170  			model
   171  			  schema 1.1
   172  
   173  			type user
   174  
   175  			type group
   176  		  	  relations
   177  			    define member: [user]
   178  
   179  			type document
   180  			  relations
   181  			    define viewer: [user, group#member]`,
   182  			tuples: []string{
   183  				"document:doc1#viewer@user:jon",
   184  				"document:doc2#viewer@user:bob",
   185  				"document:doc3#viewer@group:openfga#member",
   186  				"group:openfga#member@user:jon",
   187  			},
   188  			expectedResult: []*reverseexpand.ReverseExpandResult{
   189  				{
   190  					Object:       "document:doc1",
   191  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   192  				},
   193  				{
   194  					Object:       "document:doc3",
   195  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   196  				},
   197  			},
   198  			expectedDSQueryCount: 3,
   199  		},
   200  		{
   201  			name: "success_with_direct_relationships_and_computed_usersets",
   202  			request: &reverseexpand.ReverseExpandRequest{
   203  				StoreID:    ulid.Make().String(),
   204  				ObjectType: "document",
   205  				Relation:   "viewer",
   206  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
   207  					Type: "user",
   208  					Id:   "jon",
   209  				}},
   210  			},
   211  			model: `
   212  			model
   213  			  schema 1.1
   214  
   215  			type user
   216  
   217  			type group
   218  			  relations
   219  			    define member: [user]
   220  
   221  			type document
   222  			  relations
   223  			    define owner: [user, group#member]
   224  			    define viewer: owner`,
   225  			tuples: []string{
   226  				"document:doc1#owner@user:jon",
   227  				"document:doc2#owner@user:bob",
   228  				"document:doc3#owner@group:openfga#member",
   229  				"group:openfga#member@user:jon",
   230  			},
   231  			expectedResult: []*reverseexpand.ReverseExpandResult{
   232  				{
   233  					Object:       "document:doc1",
   234  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   235  				},
   236  				{
   237  					Object:       "document:doc3",
   238  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   239  				},
   240  			},
   241  			expectedDSQueryCount: 3,
   242  		},
   243  		{
   244  			name: "success_with_many_tuples",
   245  			request: &reverseexpand.ReverseExpandRequest{
   246  				StoreID:    ulid.Make().String(),
   247  				ObjectType: "document",
   248  				Relation:   "viewer",
   249  				User: &reverseexpand.UserRefObject{
   250  					Object: &openfgav1.Object{
   251  						Type: "user",
   252  						Id:   "jon",
   253  					},
   254  				},
   255  				ContextualTuples: []*openfgav1.TupleKey{
   256  					tuple.NewTupleKey("folder:folder5", "parent", "folder:folder4"),
   257  					tuple.NewTupleKey("folder:folder6", "viewer", "user:bob"),
   258  				},
   259  			},
   260  			model: `
   261  			model
   262  			  schema 1.1
   263  
   264  			type user
   265  
   266  			type group
   267  			  relations
   268  			    define member: [user, group#member]
   269  
   270  			type folder
   271  			  relations
   272  			    define parent: [folder]
   273  			    define viewer: [user, group#member] or viewer from parent
   274  
   275  			type document
   276  			  relations
   277  			    define parent: [folder]
   278  			    define viewer: viewer from parent`,
   279  			tuples: []string{
   280  				"folder:folder1#viewer@user:jon",
   281  				"folder:folder2#parent@folder:folder1",
   282  				"folder:folder3#parent@folder:folder2",
   283  				"folder:folder4#viewer@group:eng#member",
   284  				"document:doc1#parent@folder:folder3",
   285  				"document:doc2#parent@folder:folder5",
   286  				"document:doc3#parent@folder:folder6",
   287  				"group:eng#member@group:openfga#member",
   288  				"group:openfga#member@user:jon",
   289  			},
   290  			expectedResult: []*reverseexpand.ReverseExpandResult{
   291  				{
   292  					Object:       "document:doc1",
   293  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   294  				},
   295  				{
   296  					Object:       "document:doc2",
   297  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   298  				},
   299  			},
   300  			expectedDSQueryCount: 16,
   301  		},
   302  		{
   303  			name: "resolve_objects_involved_in_recursive_hierarchy",
   304  			request: &reverseexpand.ReverseExpandRequest{
   305  				StoreID:    ulid.Make().String(),
   306  				ObjectType: "folder",
   307  				Relation:   "viewer",
   308  				User: &reverseexpand.UserRefObject{
   309  					Object: &openfgav1.Object{
   310  						Type: "user",
   311  						Id:   "jon",
   312  					},
   313  				},
   314  			},
   315  			model: `
   316  			model
   317  			  schema 1.1
   318  
   319  			type user
   320  
   321  			type folder
   322  			  relations
   323  			    define parent: [folder]
   324  			    define viewer: [user] or viewer from parent`,
   325  			tuples: []string{
   326  				"folder:folder1#viewer@user:jon",
   327  				"folder:folder2#parent@folder:folder1",
   328  				"folder:folder3#parent@folder:folder2",
   329  			},
   330  			expectedResult: []*reverseexpand.ReverseExpandResult{
   331  				{
   332  					Object:       "folder:folder1",
   333  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   334  				},
   335  				{
   336  					Object:       "folder:folder2",
   337  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   338  				},
   339  				{
   340  					Object:       "folder:folder3",
   341  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   342  				},
   343  			},
   344  			expectedDSQueryCount: 4,
   345  		},
   346  		{
   347  			name: "resolution_depth_exceeded_failure",
   348  			request: &reverseexpand.ReverseExpandRequest{
   349  				StoreID:    ulid.Make().String(),
   350  				ObjectType: "folder",
   351  				Relation:   "viewer",
   352  				User: &reverseexpand.UserRefObject{
   353  					Object: &openfgav1.Object{
   354  						Type: "user",
   355  						Id:   "jon",
   356  					},
   357  				},
   358  			},
   359  			resolveNodeLimit: 2,
   360  			model: `
   361  			model
   362  			  schema 1.1
   363  
   364  			type user
   365  
   366  			type folder
   367  			  relations
   368  			    define parent: [folder]
   369  			    define viewer: [user] or viewer from parent`,
   370  			tuples: []string{
   371  				"folder:folder1#viewer@user:jon",
   372  				"folder:folder2#parent@folder:folder1",
   373  				"folder:folder3#parent@folder:folder2",
   374  			},
   375  			expectedError:        graph.ErrResolutionDepthExceeded,
   376  			expectedDSQueryCount: 0,
   377  		},
   378  		{
   379  			name: "objects_connected_to_a_userset",
   380  			request: &reverseexpand.ReverseExpandRequest{
   381  				StoreID:    ulid.Make().String(),
   382  				ObjectType: "group",
   383  				Relation:   "member",
   384  				User: &reverseexpand.UserRefObjectRelation{
   385  					ObjectRelation: &openfgav1.ObjectRelation{
   386  						Object:   "group:iam",
   387  						Relation: "member",
   388  					},
   389  				},
   390  			},
   391  			model: `
   392  			model
   393  			  schema 1.1
   394  
   395  			type user
   396  
   397  			type group
   398  			  relations
   399  			    define member: [user, group#member]`,
   400  			tuples: []string{
   401  				"group:opensource#member@group:eng#member",
   402  				"group:eng#member@group:iam#member",
   403  				"group:iam#member@user:jon",
   404  			},
   405  			expectedResult: []*reverseexpand.ReverseExpandResult{
   406  				{
   407  					Object:       "group:opensource",
   408  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   409  				},
   410  				{
   411  					Object:       "group:eng",
   412  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   413  				},
   414  				{
   415  					Object:       "group:iam",
   416  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   417  				},
   418  			},
   419  			expectedDSQueryCount: 3,
   420  		},
   421  		{
   422  			name: "objects_connected_to_a_userset_self_referencing",
   423  			request: &reverseexpand.ReverseExpandRequest{
   424  				StoreID:    ulid.Make().String(),
   425  				ObjectType: "group",
   426  				Relation:   "member",
   427  				User: &reverseexpand.UserRefObjectRelation{
   428  					ObjectRelation: &openfgav1.ObjectRelation{
   429  						Object:   "group:iam",
   430  						Relation: "member",
   431  					},
   432  				},
   433  			},
   434  			model: `
   435  			model
   436  			  schema 1.1
   437  
   438  			type group
   439  			  relations
   440  			    define member: [group#member]`,
   441  			tuples: []string{
   442  				"group:iam#member@group:iam#member",
   443  			},
   444  			expectedResult: []*reverseexpand.ReverseExpandResult{
   445  				{
   446  					Object:       "group:iam",
   447  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   448  				},
   449  			},
   450  			expectedDSQueryCount: 2,
   451  		},
   452  		{
   453  			name: "objects_connected_through_a_computed_userset_1",
   454  			request: &reverseexpand.ReverseExpandRequest{
   455  				StoreID:    ulid.Make().String(),
   456  				ObjectType: "document",
   457  				Relation:   "viewer",
   458  				User: &reverseexpand.UserRefObject{
   459  					Object: &openfgav1.Object{
   460  						Type: "user",
   461  						Id:   "jon",
   462  					},
   463  				},
   464  			},
   465  			model: `
   466  			model
   467  			  schema 1.1
   468  
   469  			type user
   470  
   471  			type document
   472  			  relations
   473  			    define owner: [user]
   474  			    define editor: owner
   475  			    define viewer: [document#editor]`,
   476  			tuples: []string{
   477  				"document:1#viewer@document:1#editor",
   478  				"document:1#owner@user:jon",
   479  			},
   480  			expectedResult: []*reverseexpand.ReverseExpandResult{
   481  				{
   482  					Object:       "document:1",
   483  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   484  				},
   485  			},
   486  			expectedDSQueryCount: 2,
   487  		},
   488  		{
   489  			name: "objects_connected_through_a_computed_userset_2",
   490  			request: &reverseexpand.ReverseExpandRequest{
   491  				StoreID:    ulid.Make().String(),
   492  				ObjectType: "document",
   493  				Relation:   "viewer",
   494  				User: &reverseexpand.UserRefObject{
   495  					Object: &openfgav1.Object{
   496  						Type: "user",
   497  						Id:   "jon",
   498  					},
   499  				},
   500  			},
   501  			model: `
   502  			model
   503  			  schema 1.1
   504  
   505  			  type user
   506  
   507  			type group
   508  			  relations
   509  			    define manager: [user]
   510  			    define member: manager
   511  
   512  			type document
   513  			  relations
   514  			    define viewer: [group#member]`,
   515  			tuples: []string{
   516  				"document:1#viewer@group:eng#member",
   517  				"group:eng#manager@user:jon",
   518  			},
   519  			expectedResult: []*reverseexpand.ReverseExpandResult{
   520  				{
   521  					Object:       "document:1",
   522  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   523  				},
   524  			},
   525  			expectedDSQueryCount: 2,
   526  		},
   527  		{
   528  			name: "objects_connected_through_a_computed_userset_3",
   529  			request: &reverseexpand.ReverseExpandRequest{
   530  				StoreID:    ulid.Make().String(),
   531  				ObjectType: "trial",
   532  				Relation:   "viewer",
   533  				User: &reverseexpand.UserRefObject{
   534  					Object: &openfgav1.Object{
   535  						Type: "user",
   536  						Id:   "fede",
   537  					},
   538  				},
   539  			},
   540  			model: `
   541  			model
   542  			  schema 1.1
   543  
   544  			type user
   545  
   546  			type team
   547  			  relations
   548  			    define admin: [user]
   549  			    define member: admin
   550  
   551  			type trial
   552  			  relations
   553  			    define editor: [team#member]
   554  			    define viewer: editor`,
   555  			tuples: []string{
   556  				"trial:1#editor@team:devs#member",
   557  				"team:devs#admin@user:fede",
   558  			},
   559  			expectedResult: []*reverseexpand.ReverseExpandResult{
   560  				{
   561  					Object:       "trial:1",
   562  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   563  				},
   564  			},
   565  			expectedDSQueryCount: 2,
   566  		},
   567  		{
   568  			name: "objects_connected_indirectly_through_a_ttu",
   569  			request: &reverseexpand.ReverseExpandRequest{
   570  				StoreID:    ulid.Make().String(),
   571  				ObjectType: "document",
   572  				Relation:   "view",
   573  				User: &reverseexpand.UserRefObject{
   574  					Object: &openfgav1.Object{
   575  						Type: "organization",
   576  						Id:   "2",
   577  					},
   578  				},
   579  			},
   580  			model: `
   581  			model
   582  			  schema 1.1
   583  
   584  			type organization
   585  			  relations
   586  			    define viewer: [organization]
   587  			    define can_view: viewer
   588  
   589  			type document
   590  			  relations
   591  			    define parent: [organization]
   592  			    define view: can_view from parent`,
   593  			tuples: []string{
   594  				"document:1#parent@organization:1",
   595  				"organization:1#viewer@organization:2",
   596  			},
   597  			expectedResult: []*reverseexpand.ReverseExpandResult{
   598  				{
   599  					Object:       "document:1",
   600  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   601  				},
   602  			},
   603  			expectedDSQueryCount: 2,
   604  		},
   605  		{
   606  			name: "directly_related_typed_wildcard",
   607  			request: &reverseexpand.ReverseExpandRequest{
   608  				StoreID:    ulid.Make().String(),
   609  				ObjectType: "document",
   610  				Relation:   "viewer",
   611  				User:       &reverseexpand.UserRefTypedWildcard{Type: "user"},
   612  			},
   613  			model: `
   614  			model
   615  			  schema 1.1
   616  
   617  			type user
   618  
   619  			type document
   620  			  relations
   621  			    define viewer: [user, user:*]`,
   622  			tuples: []string{
   623  				"document:1#viewer@user:*",
   624  				"document:2#viewer@user:*",
   625  				"document:3#viewer@user:jon",
   626  			},
   627  			expectedResult: []*reverseexpand.ReverseExpandResult{
   628  				{
   629  					Object:       "document:1",
   630  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   631  				},
   632  				{
   633  					Object:       "document:2",
   634  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   635  				},
   636  			},
   637  			expectedDSQueryCount: 1,
   638  		},
   639  		{
   640  			name: "indirectly_related_typed_wildcard",
   641  			request: &reverseexpand.ReverseExpandRequest{
   642  				StoreID:    ulid.Make().String(),
   643  				ObjectType: "document",
   644  				Relation:   "viewer",
   645  				User:       &reverseexpand.UserRefTypedWildcard{Type: "user"},
   646  			},
   647  			model: `
   648  			model
   649  			  schema 1.1
   650  
   651  			type user
   652  
   653  			type group
   654  			  relations
   655  			    define member: [user:*]
   656  
   657  			type document
   658  			  relations
   659  			    define viewer: [group#member]`,
   660  			tuples: []string{
   661  				"document:1#viewer@group:eng#member",
   662  				"document:2#viewer@group:fga#member",
   663  				"group:eng#member@user:*",
   664  			},
   665  			expectedResult: []*reverseexpand.ReverseExpandResult{
   666  				{
   667  					Object:       "document:1",
   668  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   669  				},
   670  			},
   671  			expectedDSQueryCount: 2,
   672  		},
   673  		{
   674  			name: "relationship_through_multiple_indirections",
   675  			request: &reverseexpand.ReverseExpandRequest{
   676  				StoreID:    ulid.Make().String(),
   677  				ObjectType: "document",
   678  				Relation:   "viewer",
   679  				User: &reverseexpand.UserRefObject{
   680  					Object: &openfgav1.Object{
   681  						Type: "user",
   682  						Id:   "jon",
   683  					},
   684  				},
   685  			},
   686  			model: `
   687  			model
   688  			  schema 1.1
   689  
   690  			type user
   691  
   692  			type team
   693  			  relations
   694  			    define member: [user]
   695  
   696  			type group
   697  			  relations
   698  			    define member: [team#member]
   699  
   700  			type document
   701  			  relations
   702  			    define viewer: [group#member]`,
   703  			tuples: []string{
   704  				"team:tigers#member@user:jon",
   705  				"group:eng#member@team:tigers#member",
   706  				"document:1#viewer@group:eng#member",
   707  			},
   708  			expectedResult: []*reverseexpand.ReverseExpandResult{
   709  				{
   710  					Object:       "document:1",
   711  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   712  				},
   713  			},
   714  			expectedDSQueryCount: 3,
   715  		},
   716  		{
   717  			name: "typed_wildcard_relationship_through_multiple_indirections",
   718  			request: &reverseexpand.ReverseExpandRequest{
   719  				StoreID:    ulid.Make().String(),
   720  				ObjectType: "document",
   721  				Relation:   "viewer",
   722  				User: &reverseexpand.UserRefObject{
   723  					Object: &openfgav1.Object{
   724  						Type: "user",
   725  						Id:   "jon",
   726  					},
   727  				},
   728  			},
   729  			model: `
   730  			model
   731  			  schema 1.1
   732  
   733  			type user
   734  
   735  			type group
   736  			  relations
   737  			    define member: [team#member]
   738  
   739  			type team
   740  			  relations
   741  			    define member: [user:*]
   742  
   743  			type document
   744  			  relations
   745  			    define viewer: [group#member]`,
   746  			tuples: []string{
   747  				"team:tigers#member@user:*",
   748  				"group:eng#member@team:tigers#member",
   749  				"document:1#viewer@group:eng#member",
   750  			},
   751  			expectedResult: []*reverseexpand.ReverseExpandResult{
   752  				{
   753  					Object:       "document:1",
   754  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   755  				},
   756  			},
   757  			expectedDSQueryCount: 3,
   758  		},
   759  		{
   760  			name: "simple_typed_wildcard_and_direct_relation",
   761  			request: &reverseexpand.ReverseExpandRequest{
   762  				StoreID:    ulid.Make().String(),
   763  				ObjectType: "document",
   764  				Relation:   "viewer",
   765  				User: &reverseexpand.UserRefObject{
   766  					Object: &openfgav1.Object{Type: "user", Id: "jon"},
   767  				},
   768  			},
   769  			model: `
   770  			model
   771  			  schema 1.1
   772  
   773  			type user
   774  
   775  			type document
   776  			  relations
   777  			    define viewer: [user, user:*]`,
   778  			tuples: []string{
   779  				"document:1#viewer@user:*",
   780  				"document:2#viewer@user:jon",
   781  			},
   782  			expectedResult: []*reverseexpand.ReverseExpandResult{
   783  				{
   784  					Object:       "document:1",
   785  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   786  				},
   787  				{
   788  					Object:       "document:2",
   789  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   790  				},
   791  			},
   792  			expectedDSQueryCount: 1,
   793  		},
   794  		{
   795  			name: "simple_typed_wildcard_and_indirect_relation",
   796  			request: &reverseexpand.ReverseExpandRequest{
   797  				StoreID:    ulid.Make().String(),
   798  				ObjectType: "document",
   799  				Relation:   "viewer",
   800  				User: &reverseexpand.UserRefObject{
   801  					Object: &openfgav1.Object{
   802  						Type: "user",
   803  						Id:   "jon",
   804  					},
   805  				},
   806  			},
   807  			model: `
   808  			model
   809  			  schema 1.1
   810  
   811  			type user
   812  
   813  			type group
   814  			  relations
   815  			    define member: [user, user:*]
   816  
   817  			type document
   818  			  relations
   819  			    define viewer: [group#member]`,
   820  			tuples: []string{
   821  				"group:eng#member@user:*",
   822  				"group:fga#member@user:jon",
   823  				"document:1#viewer@group:eng#member",
   824  				"document:2#viewer@group:fga#member",
   825  			},
   826  			expectedResult: []*reverseexpand.ReverseExpandResult{
   827  				{
   828  					Object:       "document:1",
   829  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   830  				},
   831  				{
   832  					Object:       "document:2",
   833  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   834  				},
   835  			},
   836  			expectedDSQueryCount: 3,
   837  		},
   838  		{
   839  			name: "with_public_user_access_1",
   840  			request: &reverseexpand.ReverseExpandRequest{
   841  				StoreID:    ulid.Make().String(),
   842  				ObjectType: "document",
   843  				Relation:   "viewer",
   844  				User: &reverseexpand.UserRefObject{
   845  					Object: &openfgav1.Object{
   846  						Type: "user",
   847  						Id:   "*",
   848  					},
   849  				},
   850  			},
   851  			model: `
   852  			model
   853  			  schema 1.1
   854  
   855  			type user
   856  
   857  			type group
   858  			  relations
   859  			    define member: [user:*]
   860  
   861  			type document
   862  			  relations
   863  			    define viewer: [group#member]`,
   864  			tuples: []string{
   865  				"group:eng#member@user:*",
   866  				"group:other#member@employee:*", // assume this comes from a prior model
   867  				"document:1#viewer@group:eng#member",
   868  				"document:2#viewer@group:fga#member",
   869  				"document:3#viewer@group:other#member",
   870  			},
   871  			expectedResult: []*reverseexpand.ReverseExpandResult{
   872  				{
   873  					Object:       "document:1",
   874  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   875  				},
   876  			},
   877  			expectedDSQueryCount: 2,
   878  		},
   879  		{
   880  			name: "with_public_user_access_2",
   881  			request: &reverseexpand.ReverseExpandRequest{
   882  				StoreID:    ulid.Make().String(),
   883  				ObjectType: "resource",
   884  				Relation:   "reader",
   885  				User: &reverseexpand.UserRefObject{
   886  					Object: &openfgav1.Object{
   887  						Type: "user",
   888  						Id:   "bev",
   889  					},
   890  				},
   891  			},
   892  			model: `
   893  			model
   894  			  schema 1.1
   895  
   896  			type user
   897  
   898  			type group
   899  			  relations
   900  			    define member: [user]
   901  
   902  			type resource
   903  			  relations
   904  			    define reader: [user, user:*, group#member] or writer
   905  			    define writer: [user, user:*, group#member]`,
   906  			tuples: []string{
   907  				"resource:x#writer@user:*",
   908  			},
   909  			expectedResult: []*reverseexpand.ReverseExpandResult{
   910  				{
   911  					Object:       "resource:x",
   912  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   913  				},
   914  			},
   915  			expectedDSQueryCount: 3,
   916  		},
   917  		{
   918  			name: "simple_typed_wildcard_with_contextual_tuples_1",
   919  			request: &reverseexpand.ReverseExpandRequest{
   920  				StoreID:    ulid.Make().String(),
   921  				ObjectType: "document",
   922  				Relation:   "viewer",
   923  				User: &reverseexpand.UserRefObject{
   924  					Object: &openfgav1.Object{Type: "user", Id: "jon"},
   925  				},
   926  				ContextualTuples: []*openfgav1.TupleKey{
   927  					tuple.NewTupleKey("document:1", "viewer", "user:*"),
   928  					tuple.NewTupleKey("document:2", "viewer", "user:jon"),
   929  				},
   930  			},
   931  			model: `
   932  			model
   933  			  schema 1.1
   934  
   935  			type user
   936  			type document
   937  			  relations
   938  			    define viewer: [user, user:*]`,
   939  			expectedResult: []*reverseexpand.ReverseExpandResult{
   940  				{
   941  					Object:       "document:1",
   942  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   943  				},
   944  				{
   945  					Object:       "document:2",
   946  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   947  				},
   948  			},
   949  			expectedDSQueryCount: 1,
   950  		},
   951  		{
   952  			name: "simple_typed_wildcard_with_contextual_tuples_2",
   953  			request: &reverseexpand.ReverseExpandRequest{
   954  				StoreID:    ulid.Make().String(),
   955  				ObjectType: "document",
   956  				Relation:   "viewer",
   957  				User:       &reverseexpand.UserRefTypedWildcard{Type: "user"},
   958  				ContextualTuples: []*openfgav1.TupleKey{
   959  					tuple.NewTupleKey("document:1", "viewer", "employee:*"),
   960  					tuple.NewTupleKey("document:2", "viewer", "user:*"),
   961  				},
   962  			},
   963  			model: `
   964  			model
   965  			  schema 1.1
   966  
   967  			type user
   968  
   969  			type employee
   970  
   971  			type document
   972  			  relations
   973  			    define viewer: [user:*]`,
   974  			expectedResult: []*reverseexpand.ReverseExpandResult{
   975  				{
   976  					Object:       "document:2",
   977  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
   978  				},
   979  			},
   980  			expectedDSQueryCount: 1,
   981  		},
   982  		{
   983  			name: "simple_typed_wildcard_with_contextual_tuples_3",
   984  			request: &reverseexpand.ReverseExpandRequest{
   985  				StoreID:    ulid.Make().String(),
   986  				ObjectType: "document",
   987  				Relation:   "viewer",
   988  				User: &reverseexpand.UserRefObjectRelation{
   989  					ObjectRelation: &openfgav1.ObjectRelation{
   990  						Object:   "group:eng",
   991  						Relation: "member",
   992  					},
   993  				},
   994  				ContextualTuples: []*openfgav1.TupleKey{
   995  					tuple.NewTupleKey("document:1", "viewer", "group:eng#member"),
   996  				},
   997  			},
   998  			model: `
   999  			model
  1000  			  schema 1.1
  1001  
  1002  			type user
  1003  
  1004  			type group
  1005  			  relations
  1006  			    define member: [user]
  1007  
  1008  			type document
  1009  			  relations
  1010  			    define viewer: [group#member]`,
  1011  			expectedResult: []*reverseexpand.ReverseExpandResult{
  1012  				{
  1013  					Object:       "document:1",
  1014  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1015  				},
  1016  			},
  1017  			expectedDSQueryCount: 1,
  1018  		},
  1019  		{
  1020  			name: "non-assignable_ttu_relationship",
  1021  			request: &reverseexpand.ReverseExpandRequest{
  1022  				StoreID:    ulid.Make().String(),
  1023  				ObjectType: "document",
  1024  				Relation:   "viewer",
  1025  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
  1026  					Type: "user",
  1027  					Id:   "jon",
  1028  				}},
  1029  			},
  1030  			model: `
  1031  			model
  1032  			  schema 1.1
  1033  
  1034  			type user
  1035  
  1036  			type folder
  1037  			  relations
  1038  			    define viewer: [user, user:*]
  1039  
  1040  			type document
  1041  			  relations
  1042  			    define parent: [folder]
  1043  			    define viewer: viewer from parent`,
  1044  			tuples: []string{
  1045  				"document:1#parent@folder:1",
  1046  				"document:2#parent@folder:2",
  1047  				"folder:1#viewer@user:jon",
  1048  				"folder:2#viewer@user:*",
  1049  			},
  1050  			expectedResult: []*reverseexpand.ReverseExpandResult{
  1051  				{
  1052  					Object:       "document:1",
  1053  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1054  				},
  1055  				{
  1056  					Object:       "document:2",
  1057  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1058  				},
  1059  			},
  1060  			expectedDSQueryCount: 3,
  1061  		},
  1062  		{
  1063  			name: "non-assignable_ttu_relationship_without_wildcard_connectivity",
  1064  			request: &reverseexpand.ReverseExpandRequest{
  1065  				StoreID:    ulid.Make().String(),
  1066  				ObjectType: "document",
  1067  				Relation:   "viewer",
  1068  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
  1069  					Type: "user",
  1070  					Id:   "jon",
  1071  				}},
  1072  			},
  1073  			model: `
  1074  			model
  1075  			  schema 1.1
  1076  
  1077  			type user
  1078  			type employee
  1079  
  1080  			type folder
  1081  			  relations
  1082  			    define viewer: [user, employee:*]
  1083  
  1084  			type document
  1085  			  relations
  1086  			    define parent: [folder]
  1087  			    define viewer: viewer from parent`,
  1088  			tuples: []string{
  1089  				"document:1#parent@folder:1",
  1090  				"document:2#parent@folder:2",
  1091  				"folder:1#viewer@user:jon",
  1092  				"folder:2#viewer@user:*",
  1093  			},
  1094  			expectedResult: []*reverseexpand.ReverseExpandResult{
  1095  				{
  1096  					Object:       "document:1",
  1097  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1098  				},
  1099  			},
  1100  			expectedDSQueryCount: 2,
  1101  		},
  1102  		{
  1103  			name: "non-assignable_ttu_relationship_through_indirection_1",
  1104  			request: &reverseexpand.ReverseExpandRequest{
  1105  				StoreID:    ulid.Make().String(),
  1106  				ObjectType: "document",
  1107  				Relation:   "viewer",
  1108  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
  1109  					Type: "user",
  1110  					Id:   "jon",
  1111  				}},
  1112  			},
  1113  			model: `
  1114  			model
  1115  			  schema 1.1
  1116  
  1117  			type user
  1118  
  1119  			type group
  1120  			  relations
  1121  			    define member: [user:*]
  1122  
  1123  			type folder
  1124  			  relations
  1125  			    define viewer: [group#member]
  1126  
  1127  			type document
  1128  			  relations
  1129  			    define parent: [folder]
  1130  			    define viewer: viewer from parent`,
  1131  			tuples: []string{
  1132  				"document:1#parent@folder:1",
  1133  				"folder:1#viewer@group:eng#member",
  1134  				"group:eng#member@user:*",
  1135  			},
  1136  			expectedResult: []*reverseexpand.ReverseExpandResult{
  1137  				{
  1138  					Object:       "document:1",
  1139  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1140  				},
  1141  			},
  1142  			expectedDSQueryCount: 3,
  1143  		},
  1144  		{
  1145  			name: "non-assignable_ttu_relationship_through_indirection_2",
  1146  			request: &reverseexpand.ReverseExpandRequest{
  1147  				StoreID:    ulid.Make().String(),
  1148  				ObjectType: "resource",
  1149  				Relation:   "writer",
  1150  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
  1151  					Type: "user",
  1152  					Id:   "anne",
  1153  				}},
  1154  			},
  1155  			model: `
  1156  			model
  1157  			  schema 1.1
  1158  
  1159  			type user
  1160  
  1161  			type org
  1162  			  relations
  1163  			    define dept: [group]
  1164  			    define dept_member: member from dept
  1165  
  1166  			type group
  1167  			  relations
  1168  			    define member: [user]
  1169  
  1170  			type resource
  1171  			  relations
  1172  			    define writer: [org#dept_member]`,
  1173  			tuples: []string{
  1174  				"resource:eng_handbook#writer@org:eng#dept_member",
  1175  				"org:eng#dept@group:fga",
  1176  				"group:fga#member@user:anne",
  1177  			},
  1178  			expectedResult: []*reverseexpand.ReverseExpandResult{
  1179  				{
  1180  					Object:       "resource:eng_handbook",
  1181  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1182  				},
  1183  			},
  1184  			expectedDSQueryCount: 3,
  1185  		},
  1186  		{
  1187  			name: "non-assignable_ttu_relationship_through_indirection_3",
  1188  			request: &reverseexpand.ReverseExpandRequest{
  1189  				StoreID:    ulid.Make().String(),
  1190  				ObjectType: "resource",
  1191  				Relation:   "reader",
  1192  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
  1193  					Type: "user",
  1194  					Id:   "anne",
  1195  				}},
  1196  			},
  1197  			model: `
  1198  			model
  1199  			  schema 1.1
  1200  
  1201  			type user
  1202  
  1203  			type org
  1204  			  relations
  1205  			    define dept: [group]
  1206  			    define dept_member: member from dept
  1207  
  1208  			type group
  1209  			  relations
  1210  			    define member: [user]
  1211  
  1212  			type resource
  1213  			  relations
  1214  			    define writer: [org#dept_member]
  1215  			    define reader: [org#dept_member] or writer`,
  1216  			tuples: []string{
  1217  				"resource:eng_handbook#writer@org:eng#dept_member",
  1218  				"org:eng#dept@group:fga",
  1219  				"group:fga#member@user:anne",
  1220  			},
  1221  			expectedResult: []*reverseexpand.ReverseExpandResult{
  1222  				{
  1223  					Object:       "resource:eng_handbook",
  1224  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1225  				},
  1226  			},
  1227  			expectedDSQueryCount: 4,
  1228  		},
  1229  		{
  1230  			name: "cyclical_tupleset_relation_terminates",
  1231  			request: &reverseexpand.ReverseExpandRequest{
  1232  				StoreID:    ulid.Make().String(),
  1233  				ObjectType: "node",
  1234  				Relation:   "editor",
  1235  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
  1236  					Type: "user",
  1237  					Id:   "wonder",
  1238  				}},
  1239  			},
  1240  			model: `
  1241  			model
  1242  			  schema 1.1
  1243  
  1244  			type user
  1245  
  1246  			type node
  1247  			  relations
  1248  			    define parent: [node]
  1249  			    define editor: [user] or editor from parent`,
  1250  			tuples: []string{
  1251  				"node:abc#editor@user:wonder",
  1252  			},
  1253  			expectedResult: []*reverseexpand.ReverseExpandResult{
  1254  				{
  1255  					Object:       "node:abc",
  1256  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1257  				},
  1258  			},
  1259  			expectedDSQueryCount: 2,
  1260  		},
  1261  		{
  1262  			name: "does_not_send_duplicate_even_though_there_are_two_paths_to_same_solution",
  1263  			request: &reverseexpand.ReverseExpandRequest{
  1264  				StoreID:    ulid.Make().String(),
  1265  				ObjectType: "document",
  1266  				Relation:   "viewer",
  1267  				User: &reverseexpand.UserRefObject{Object: &openfgav1.Object{
  1268  					Type: "user",
  1269  					Id:   "jon",
  1270  				}},
  1271  			},
  1272  			model: `
  1273  			model
  1274  			  schema 1.1
  1275  
  1276  			type user
  1277  
  1278  			type group
  1279  			  relations
  1280  			    define member: [user]
  1281  			    define maintainer: [user]
  1282  
  1283  			type document
  1284  			  relations
  1285  			    define viewer: [group#member,group#maintainer]`,
  1286  			tuples: []string{
  1287  				"document:1#viewer@group:example1#maintainer",
  1288  				"group:example1#maintainer@user:jon",
  1289  				"group:example1#member@user:jon",
  1290  			},
  1291  			expectedResult: []*reverseexpand.ReverseExpandResult{
  1292  				{
  1293  					Object:       "document:1",
  1294  					ResultStatus: reverseexpand.NoFurtherEvalStatus,
  1295  				},
  1296  			},
  1297  			expectedDSQueryCount: 4,
  1298  		},
  1299  	}
  1300  
  1301  	for _, test := range tests {
  1302  		t.Run(test.name, func(t *testing.T) {
  1303  			storeID, model := storagetest.BootstrapFGAStore(t, ds, test.model, test.tuples)
  1304  			test.request.StoreID = storeID
  1305  
  1306  			var opts []reverseexpand.ReverseExpandQueryOption
  1307  
  1308  			if test.resolveNodeLimit != 0 {
  1309  				opts = append(opts, reverseexpand.WithResolveNodeLimit(test.resolveNodeLimit))
  1310  			}
  1311  
  1312  			reverseExpandQuery := reverseexpand.NewReverseExpandQuery(ds, typesystem.New(model), opts...)
  1313  
  1314  			resultChan := make(chan *reverseexpand.ReverseExpandResult, 100)
  1315  
  1316  			timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  1317  			defer cancel()
  1318  
  1319  			resolutionMetadata := reverseexpand.NewResolutionMetadata()
  1320  
  1321  			reverseExpandErrCh := make(chan error, 1)
  1322  			go func() {
  1323  				errReverseExpand := reverseExpandQuery.Execute(timeoutCtx, test.request, resultChan, resolutionMetadata)
  1324  				if errReverseExpand != nil {
  1325  					reverseExpandErrCh <- errReverseExpand
  1326  				}
  1327  			}()
  1328  
  1329  			var results []*reverseexpand.ReverseExpandResult
  1330  
  1331  			for {
  1332  				select {
  1333  				case errFromChannel := <-reverseExpandErrCh:
  1334  					if errors.Is(errFromChannel, context.DeadlineExceeded) {
  1335  						require.FailNow(t, "unexpected timeout")
  1336  					}
  1337  					require.ErrorIs(t, errFromChannel, test.expectedError)
  1338  					return
  1339  				case res, channelOpen := <-resultChan:
  1340  					if !channelOpen {
  1341  						t.Log("channel closed")
  1342  						if test.expectedError == nil {
  1343  							require.ElementsMatch(t, test.expectedResult, results)
  1344  							require.Equal(t, test.expectedDSQueryCount, *resolutionMetadata.DatastoreQueryCount)
  1345  						} else {
  1346  							require.FailNow(t, "expected an error, got none")
  1347  						}
  1348  						return
  1349  					} else {
  1350  						t.Logf("appending result %s", res.Object)
  1351  						results = append(results, res)
  1352  					}
  1353  				}
  1354  			}
  1355  		})
  1356  	}
  1357  }