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

     1  package test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	"github.com/oklog/ulid/v2"
    10  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
    11  	"github.com/stretchr/testify/require"
    12  	"google.golang.org/protobuf/testing/protocmp"
    13  
    14  	"github.com/openfga/openfga/pkg/server/commands"
    15  	serverErrors "github.com/openfga/openfga/pkg/server/errors"
    16  	"github.com/openfga/openfga/pkg/storage"
    17  	"github.com/openfga/openfga/pkg/testutils"
    18  	"github.com/openfga/openfga/pkg/tuple"
    19  	"github.com/openfga/openfga/pkg/typesystem"
    20  )
    21  
    22  func TestExpandQuery(t *testing.T, datastore storage.OpenFGADatastore) {
    23  	tests := []struct {
    24  		name     string
    25  		model    *openfgav1.AuthorizationModel
    26  		tuples   []*openfgav1.TupleKey
    27  		request  *openfgav1.ExpandRequest
    28  		expected *openfgav1.ExpandResponse
    29  	}{
    30  		{
    31  			name: "1.1_simple_direct",
    32  			model: testutils.MustTransformDSLToProtoWithID(`
    33  				model
    34  					schema 1.1
    35  				type user
    36  				type repo
    37  					relations
    38  						define admin: [user]`),
    39  			tuples: []*openfgav1.TupleKey{
    40  				{
    41  					Object:   "repo:openfga/foo",
    42  					Relation: "admin",
    43  					User:     "user:jon",
    44  				},
    45  			},
    46  			request: &openfgav1.ExpandRequest{
    47  				TupleKey: tuple.NewExpandRequestTupleKey(
    48  					"repo:openfga/foo",
    49  					"admin",
    50  				),
    51  			},
    52  			expected: &openfgav1.ExpandResponse{
    53  				Tree: &openfgav1.UsersetTree{
    54  					Root: &openfgav1.UsersetTree_Node{
    55  						Name: "repo:openfga/foo#admin",
    56  						Value: &openfgav1.UsersetTree_Node_Leaf{
    57  							Leaf: &openfgav1.UsersetTree_Leaf{
    58  								Value: &openfgav1.UsersetTree_Leaf_Users{
    59  									Users: &openfgav1.UsersetTree_Users{
    60  										Users: []string{"user:jon"},
    61  									},
    62  								},
    63  							},
    64  						},
    65  					},
    66  				},
    67  			},
    68  		},
    69  		{
    70  			name: "1.1_computed_userset",
    71  			model: testutils.MustTransformDSLToProtoWithID(`
    72  				model
    73  					schema 1.1
    74  				type user
    75  				type repo
    76  					relations
    77  						define admin: [user]
    78  						define writer: admin`),
    79  			tuples: []*openfgav1.TupleKey{},
    80  			request: &openfgav1.ExpandRequest{
    81  				TupleKey: tuple.NewExpandRequestTupleKey(
    82  					"repo:openfga/foo",
    83  					"writer",
    84  				),
    85  			},
    86  			expected: &openfgav1.ExpandResponse{
    87  				Tree: &openfgav1.UsersetTree{
    88  					Root: &openfgav1.UsersetTree_Node{
    89  						Name: "repo:openfga/foo#writer",
    90  						Value: &openfgav1.UsersetTree_Node_Leaf{
    91  							Leaf: &openfgav1.UsersetTree_Leaf{
    92  								Value: &openfgav1.UsersetTree_Leaf_Computed{
    93  									Computed: &openfgav1.UsersetTree_Computed{
    94  										Userset: "repo:openfga/foo#admin",
    95  									},
    96  								},
    97  							},
    98  						},
    99  					},
   100  				},
   101  			},
   102  		},
   103  		{
   104  			name: "1.1_tuple_to_userset",
   105  			model: testutils.MustTransformDSLToProtoWithID(`
   106  				model
   107  					schema 1.1
   108  				type user
   109  				type repo
   110  					relations
   111  						define admin: repo_admin from manager
   112  						define manager: [org]
   113  				type org
   114  					relations
   115  						define repo_admin: [user]`),
   116  			tuples: []*openfgav1.TupleKey{
   117  				{
   118  					Object:   "repo:openfga/foo",
   119  					Relation: "manager",
   120  					User:     "org:openfga",
   121  				},
   122  				{
   123  					Object:   "org:openfga",
   124  					Relation: "repo_admin",
   125  					User:     "user:jon",
   126  				},
   127  			},
   128  			request: &openfgav1.ExpandRequest{
   129  				TupleKey: tuple.NewExpandRequestTupleKey(
   130  					"repo:openfga/foo",
   131  					"admin",
   132  				),
   133  			},
   134  			expected: &openfgav1.ExpandResponse{
   135  				Tree: &openfgav1.UsersetTree{
   136  					Root: &openfgav1.UsersetTree_Node{
   137  						Name: "repo:openfga/foo#admin",
   138  						Value: &openfgav1.UsersetTree_Node_Leaf{
   139  							Leaf: &openfgav1.UsersetTree_Leaf{
   140  								Value: &openfgav1.UsersetTree_Leaf_TupleToUserset{
   141  									TupleToUserset: &openfgav1.UsersetTree_TupleToUserset{
   142  										Tupleset: "repo:openfga/foo#manager",
   143  										Computed: []*openfgav1.UsersetTree_Computed{
   144  											{
   145  												Userset: "org:openfga#repo_admin",
   146  											},
   147  										},
   148  									},
   149  								},
   150  							},
   151  						},
   152  					},
   153  				},
   154  			},
   155  		},
   156  		{
   157  			name: "1.1_tuple_to_userset_II",
   158  			model: testutils.MustTransformDSLToProtoWithID(`
   159  				model
   160  					schema 1.1
   161  				type user
   162  				type repo
   163  					relations
   164  						define admin: repo_admin from manager
   165  						define manager: [org]
   166  				type org
   167  					relations
   168  						define repo_admin: [user]`),
   169  			tuples: []*openfgav1.TupleKey{
   170  				{
   171  					Object:   "repo:openfga/foo",
   172  					Relation: "manager",
   173  					User:     "org:openfga",
   174  				},
   175  				{
   176  					Object:   "org:openfga",
   177  					Relation: "repo_admin",
   178  					User:     "user:jon",
   179  				},
   180  				{
   181  					Object:   "repo:openfga/foo",
   182  					Relation: "manager",
   183  					User:     "amy", // should be skipped since it's not a valid target for a tupleset relation
   184  				},
   185  			},
   186  			request: &openfgav1.ExpandRequest{
   187  				TupleKey: tuple.NewExpandRequestTupleKey(
   188  					"repo:openfga/foo",
   189  					"admin",
   190  				),
   191  			},
   192  			expected: &openfgav1.ExpandResponse{
   193  				Tree: &openfgav1.UsersetTree{
   194  					Root: &openfgav1.UsersetTree_Node{
   195  						Name: "repo:openfga/foo#admin",
   196  						Value: &openfgav1.UsersetTree_Node_Leaf{
   197  							Leaf: &openfgav1.UsersetTree_Leaf{
   198  								Value: &openfgav1.UsersetTree_Leaf_TupleToUserset{
   199  									TupleToUserset: &openfgav1.UsersetTree_TupleToUserset{
   200  										Tupleset: "repo:openfga/foo#manager",
   201  										Computed: []*openfgav1.UsersetTree_Computed{
   202  											{
   203  												Userset: "org:openfga#repo_admin",
   204  											},
   205  										},
   206  									},
   207  								},
   208  							},
   209  						},
   210  					},
   211  				},
   212  			},
   213  		},
   214  		{
   215  			name: "1.1_tuple_to_userset_implicit",
   216  			model: testutils.MustTransformDSLToProtoWithID(`
   217  				model
   218  					schema 1.1
   219  				type user
   220  				type repo
   221  					relations
   222  						define admin: repo_admin from manager
   223  						define manager: [org]
   224  				type org
   225  					relations
   226  						define repo_admin: [user]`),
   227  			tuples: []*openfgav1.TupleKey{
   228  				{
   229  					Object:   "repo:openfga/foo",
   230  					Relation: "manager",
   231  					User:     "org:openfga",
   232  				},
   233  				{
   234  					Object:   "org:openfga",
   235  					Relation: "repo_admin",
   236  					User:     "user:jon",
   237  				},
   238  			},
   239  			request: &openfgav1.ExpandRequest{
   240  				TupleKey: tuple.NewExpandRequestTupleKey(
   241  					"repo:openfga/foo",
   242  					"admin",
   243  				),
   244  			},
   245  			expected: &openfgav1.ExpandResponse{
   246  				Tree: &openfgav1.UsersetTree{
   247  					Root: &openfgav1.UsersetTree_Node{
   248  						Name: "repo:openfga/foo#admin",
   249  						Value: &openfgav1.UsersetTree_Node_Leaf{
   250  							Leaf: &openfgav1.UsersetTree_Leaf{
   251  								Value: &openfgav1.UsersetTree_Leaf_TupleToUserset{
   252  									TupleToUserset: &openfgav1.UsersetTree_TupleToUserset{
   253  										Tupleset: "repo:openfga/foo#manager",
   254  										Computed: []*openfgav1.UsersetTree_Computed{
   255  											{
   256  												Userset: "org:openfga#repo_admin",
   257  											},
   258  										},
   259  									},
   260  								},
   261  							},
   262  						},
   263  					},
   264  				},
   265  			},
   266  		},
   267  		{
   268  			name: "1.1_simple_union",
   269  			model: testutils.MustTransformDSLToProtoWithID(`
   270  				model
   271  					schema 1.1
   272  				type user
   273  				type repo
   274  					relations
   275  						define admin: [user]
   276  						define writer: [user] or admin
   277  				type org
   278  					relations
   279  						define repo_admin: [user]`),
   280  			tuples: []*openfgav1.TupleKey{
   281  				{
   282  					Object:   "repo:openfga/foo",
   283  					Relation: "writer",
   284  					User:     "user:jon",
   285  				},
   286  			},
   287  			request: &openfgav1.ExpandRequest{
   288  				TupleKey: tuple.NewExpandRequestTupleKey(
   289  					"repo:openfga/foo",
   290  					"writer",
   291  				),
   292  			},
   293  			expected: &openfgav1.ExpandResponse{
   294  				Tree: &openfgav1.UsersetTree{
   295  					Root: &openfgav1.UsersetTree_Node{
   296  						Name: "repo:openfga/foo#writer",
   297  						Value: &openfgav1.UsersetTree_Node_Union{
   298  							Union: &openfgav1.UsersetTree_Nodes{
   299  								Nodes: []*openfgav1.UsersetTree_Node{
   300  									{
   301  										Name: "repo:openfga/foo#writer",
   302  										Value: &openfgav1.UsersetTree_Node_Leaf{
   303  											Leaf: &openfgav1.UsersetTree_Leaf{
   304  												Value: &openfgav1.UsersetTree_Leaf_Users{
   305  													Users: &openfgav1.UsersetTree_Users{
   306  														Users: []string{"user:jon"},
   307  													},
   308  												},
   309  											},
   310  										},
   311  									},
   312  									{
   313  										Name: "repo:openfga/foo#writer",
   314  										Value: &openfgav1.UsersetTree_Node_Leaf{
   315  											Leaf: &openfgav1.UsersetTree_Leaf{
   316  												Value: &openfgav1.UsersetTree_Leaf_Computed{
   317  													Computed: &openfgav1.UsersetTree_Computed{
   318  														Userset: "repo:openfga/foo#admin",
   319  													},
   320  												},
   321  											},
   322  										},
   323  									},
   324  								},
   325  							},
   326  						},
   327  					},
   328  				},
   329  			},
   330  		},
   331  		{
   332  			name: "1.1_simple_difference",
   333  			model: testutils.MustTransformDSLToProtoWithID(`
   334  				model
   335  					schema 1.1
   336  				type user
   337  				type repo
   338  					relations
   339  						define admin: [user]
   340  						define banned: [user]
   341  						define active_admin: admin but not banned`),
   342  			tuples: []*openfgav1.TupleKey{},
   343  			request: &openfgav1.ExpandRequest{
   344  				TupleKey: tuple.NewExpandRequestTupleKey(
   345  					"repo:openfga/foo",
   346  					"active_admin",
   347  				),
   348  			},
   349  			expected: &openfgav1.ExpandResponse{
   350  				Tree: &openfgav1.UsersetTree{
   351  					Root: &openfgav1.UsersetTree_Node{
   352  						Name: "repo:openfga/foo#active_admin",
   353  						Value: &openfgav1.UsersetTree_Node_Difference{
   354  							Difference: &openfgav1.UsersetTree_Difference{
   355  								Base: &openfgav1.UsersetTree_Node{
   356  									Name: "repo:openfga/foo#active_admin",
   357  									Value: &openfgav1.UsersetTree_Node_Leaf{
   358  										Leaf: &openfgav1.UsersetTree_Leaf{
   359  											Value: &openfgav1.UsersetTree_Leaf_Computed{
   360  												Computed: &openfgav1.UsersetTree_Computed{
   361  													Userset: "repo:openfga/foo#admin",
   362  												},
   363  											},
   364  										},
   365  									},
   366  								},
   367  								Subtract: &openfgav1.UsersetTree_Node{
   368  									Name: "repo:openfga/foo#active_admin",
   369  									Value: &openfgav1.UsersetTree_Node_Leaf{
   370  										Leaf: &openfgav1.UsersetTree_Leaf{
   371  											Value: &openfgav1.UsersetTree_Leaf_Computed{
   372  												Computed: &openfgav1.UsersetTree_Computed{
   373  													Userset: "repo:openfga/foo#banned",
   374  												},
   375  											},
   376  										},
   377  									},
   378  								},
   379  							},
   380  						},
   381  					},
   382  				},
   383  			},
   384  		},
   385  		{
   386  			name: "1.1_intersection",
   387  			model: testutils.MustTransformDSLToProtoWithID(`
   388  				model
   389  				  schema 1.1
   390  				type user
   391  				type repo
   392  					relations
   393  						define admin: [user]
   394  						define writer: [user] and admin`),
   395  			tuples: []*openfgav1.TupleKey{},
   396  			request: &openfgav1.ExpandRequest{
   397  				TupleKey: tuple.NewExpandRequestTupleKey(
   398  					"repo:openfga/foo",
   399  					"writer",
   400  				),
   401  			},
   402  			expected: &openfgav1.ExpandResponse{
   403  				Tree: &openfgav1.UsersetTree{
   404  					Root: &openfgav1.UsersetTree_Node{
   405  						Name: "repo:openfga/foo#writer",
   406  						Value: &openfgav1.UsersetTree_Node_Intersection{
   407  							Intersection: &openfgav1.UsersetTree_Nodes{
   408  								Nodes: []*openfgav1.UsersetTree_Node{
   409  									{
   410  										Name: "repo:openfga/foo#writer",
   411  										Value: &openfgav1.UsersetTree_Node_Leaf{
   412  											Leaf: &openfgav1.UsersetTree_Leaf{
   413  												Value: &openfgav1.UsersetTree_Leaf_Users{
   414  													Users: &openfgav1.UsersetTree_Users{
   415  														Users: []string{},
   416  													},
   417  												},
   418  											},
   419  										},
   420  									},
   421  									{
   422  										Name: "repo:openfga/foo#writer",
   423  										Value: &openfgav1.UsersetTree_Node_Leaf{
   424  											Leaf: &openfgav1.UsersetTree_Leaf{
   425  												Value: &openfgav1.UsersetTree_Leaf_Computed{
   426  													Computed: &openfgav1.UsersetTree_Computed{
   427  														Userset: "repo:openfga/foo#admin",
   428  													},
   429  												},
   430  											},
   431  										},
   432  									},
   433  								},
   434  							},
   435  						},
   436  					},
   437  				},
   438  			},
   439  		},
   440  		{
   441  			name: "1.1_complex_tree",
   442  			model: testutils.MustTransformDSLToProtoWithID(`
   443  				model
   444  				  schema 1.1
   445  				type user
   446  				type repo
   447  					relations
   448  						define admin: [user]
   449  						define owner: [org]
   450  						define banned_writer: [user]
   451  						define writer: ([user] or repo_writer from owner) but not banned_writer
   452  				type org
   453  					relations
   454  						define repo_writer: [user]`),
   455  			tuples: []*openfgav1.TupleKey{
   456  				{
   457  					Object:   "repo:openfga/foo",
   458  					Relation: "owner",
   459  					User:     "org:openfga",
   460  				},
   461  				{
   462  					Object:   "repo:openfga/foo",
   463  					Relation: "writer",
   464  					User:     "user:jon",
   465  				},
   466  			},
   467  			request: &openfgav1.ExpandRequest{
   468  				TupleKey: tuple.NewExpandRequestTupleKey(
   469  					"repo:openfga/foo",
   470  					"writer",
   471  				),
   472  			},
   473  			expected: &openfgav1.ExpandResponse{
   474  				Tree: &openfgav1.UsersetTree{
   475  					Root: &openfgav1.UsersetTree_Node{
   476  						Name: "repo:openfga/foo#writer",
   477  						Value: &openfgav1.UsersetTree_Node_Difference{
   478  							Difference: &openfgav1.UsersetTree_Difference{
   479  								Base: &openfgav1.UsersetTree_Node{
   480  									Name: "repo:openfga/foo#writer",
   481  									Value: &openfgav1.UsersetTree_Node_Union{
   482  										Union: &openfgav1.UsersetTree_Nodes{
   483  											Nodes: []*openfgav1.UsersetTree_Node{
   484  												{
   485  													Name: "repo:openfga/foo#writer",
   486  													Value: &openfgav1.UsersetTree_Node_Leaf{
   487  														Leaf: &openfgav1.UsersetTree_Leaf{
   488  															Value: &openfgav1.UsersetTree_Leaf_Users{
   489  																Users: &openfgav1.UsersetTree_Users{
   490  																	Users: []string{"user:jon"},
   491  																},
   492  															},
   493  														},
   494  													},
   495  												},
   496  												{
   497  													Name: "repo:openfga/foo#writer",
   498  													Value: &openfgav1.UsersetTree_Node_Leaf{
   499  														Leaf: &openfgav1.UsersetTree_Leaf{
   500  															Value: &openfgav1.UsersetTree_Leaf_TupleToUserset{
   501  																TupleToUserset: &openfgav1.UsersetTree_TupleToUserset{
   502  																	Tupleset: "repo:openfga/foo#owner",
   503  																	Computed: []*openfgav1.UsersetTree_Computed{
   504  																		{Userset: "org:openfga#repo_writer"},
   505  																	},
   506  																},
   507  															},
   508  														},
   509  													},
   510  												},
   511  											},
   512  										},
   513  									},
   514  								},
   515  								Subtract: &openfgav1.UsersetTree_Node{
   516  									Name: "repo:openfga/foo#writer",
   517  									Value: &openfgav1.UsersetTree_Node_Leaf{
   518  										Leaf: &openfgav1.UsersetTree_Leaf{
   519  											Value: &openfgav1.UsersetTree_Leaf_Computed{
   520  												Computed: &openfgav1.UsersetTree_Computed{
   521  													Userset: "repo:openfga/foo#banned_writer",
   522  												},
   523  											},
   524  										},
   525  									},
   526  								},
   527  							},
   528  						},
   529  					},
   530  				},
   531  			},
   532  		},
   533  		{
   534  			name: "1.1_Tuple_involving_userset_that_is_not_involved_in_TTU_rewrite",
   535  			model: testutils.MustTransformDSLToProtoWithID(`
   536  				model
   537  				  schema 1.1
   538  				type user
   539  				type document
   540  					relations
   541  						define parent: [document#editor]
   542  						define editor: [user]`),
   543  			tuples: []*openfgav1.TupleKey{
   544  				tuple.NewTupleKey("document:1", "parent", "document:2#editor"),
   545  			},
   546  			request: &openfgav1.ExpandRequest{
   547  				TupleKey: tuple.NewExpandRequestTupleKey("document:1", "parent"),
   548  			},
   549  			expected: &openfgav1.ExpandResponse{
   550  				Tree: &openfgav1.UsersetTree{
   551  					Root: &openfgav1.UsersetTree_Node{
   552  						Name: "document:1#parent",
   553  						Value: &openfgav1.UsersetTree_Node_Leaf{
   554  							Leaf: &openfgav1.UsersetTree_Leaf{
   555  								Value: &openfgav1.UsersetTree_Leaf_Users{
   556  									Users: &openfgav1.UsersetTree_Users{
   557  										Users: []string{"document:2#editor"},
   558  									},
   559  								},
   560  							},
   561  						},
   562  					},
   563  				},
   564  			},
   565  		},
   566  		{
   567  			name: "self_defined_userset_not_returned",
   568  			model: testutils.MustTransformDSLToProtoWithID(`
   569  			model
   570  				schema 1.1
   571  			type user
   572  			type group
   573  				relations
   574  					define viewer: [user]
   575  			`),
   576  			tuples: []*openfgav1.TupleKey{},
   577  			request: &openfgav1.ExpandRequest{
   578  				TupleKey: tuple.NewExpandRequestTupleKey("group:1", "viewer"),
   579  			},
   580  			expected: &openfgav1.ExpandResponse{
   581  				Tree: &openfgav1.UsersetTree{
   582  					Root: &openfgav1.UsersetTree_Node{
   583  						Name: "group:1#viewer",
   584  						Value: &openfgav1.UsersetTree_Node_Leaf{
   585  							Leaf: &openfgav1.UsersetTree_Leaf{
   586  								Value: &openfgav1.UsersetTree_Leaf_Users{
   587  									Users: &openfgav1.UsersetTree_Users{
   588  										// group:1#viewer isn't included because it's implicit
   589  										Users: []string{},
   590  									},
   591  								},
   592  							},
   593  						},
   594  					},
   595  				},
   596  			},
   597  		},
   598  		{
   599  			name: "self_defined_userset_not_returned_even_if_tuple_written",
   600  			model: testutils.MustTransformDSLToProtoWithID(`
   601  			model
   602  				schema 1.1
   603  			type user
   604  			type group
   605  				relations
   606  					define viewer: [user]
   607  			`),
   608  			tuples: []*openfgav1.TupleKey{
   609  				tuple.NewTupleKey("group:1", "viewer", "group:1#viewer"), // invalid, so should be skipped over
   610  			},
   611  			request: &openfgav1.ExpandRequest{
   612  				TupleKey: tuple.NewExpandRequestTupleKey("group:1", "viewer"),
   613  			},
   614  			expected: &openfgav1.ExpandResponse{
   615  				Tree: &openfgav1.UsersetTree{
   616  					Root: &openfgav1.UsersetTree_Node{
   617  						Name: "group:1#viewer",
   618  						Value: &openfgav1.UsersetTree_Node_Leaf{
   619  							Leaf: &openfgav1.UsersetTree_Leaf{
   620  								Value: &openfgav1.UsersetTree_Leaf_Users{
   621  									Users: &openfgav1.UsersetTree_Users{},
   622  								},
   623  							},
   624  						},
   625  					},
   626  				},
   627  			},
   628  		},
   629  		{
   630  			name: "self_defined_userset_returned_if_tuple_written",
   631  			model: testutils.MustTransformDSLToProtoWithID(`
   632  			model
   633  				schema 1.1
   634  			type user
   635  			type group
   636  				relations
   637  					define viewer: [user, group#viewer]
   638  			`),
   639  			tuples: []*openfgav1.TupleKey{
   640  				tuple.NewTupleKey("group:1", "viewer", "group:1#viewer"),
   641  			},
   642  			request: &openfgav1.ExpandRequest{
   643  				TupleKey: tuple.NewExpandRequestTupleKey("group:1", "viewer"),
   644  			},
   645  			expected: &openfgav1.ExpandResponse{
   646  				Tree: &openfgav1.UsersetTree{
   647  					Root: &openfgav1.UsersetTree_Node{
   648  						Name: "group:1#viewer",
   649  						Value: &openfgav1.UsersetTree_Node_Leaf{
   650  							Leaf: &openfgav1.UsersetTree_Leaf{
   651  								Value: &openfgav1.UsersetTree_Leaf_Users{
   652  									Users: &openfgav1.UsersetTree_Users{
   653  										Users: []string{"group:1#viewer"},
   654  									},
   655  								},
   656  							},
   657  						},
   658  					},
   659  				},
   660  			},
   661  		},
   662  		{
   663  			name: "cyclical_tuples",
   664  			model: testutils.MustTransformDSLToProtoWithID(`
   665  			model
   666  				schema 1.1
   667  			type user
   668  			type group
   669  				relations
   670  					define viewer: [user, group#viewer]
   671  			`),
   672  			tuples: []*openfgav1.TupleKey{
   673  				tuple.NewTupleKey("group:2", "viewer", "group:3#viewer"),
   674  				tuple.NewTupleKey("group:1", "viewer", "group:2#viewer"),
   675  				tuple.NewTupleKey("group:3", "viewer", "group:1#viewer"),
   676  			},
   677  			request: &openfgav1.ExpandRequest{
   678  				TupleKey: tuple.NewExpandRequestTupleKey("group:1", "viewer"),
   679  			},
   680  			expected: &openfgav1.ExpandResponse{
   681  				Tree: &openfgav1.UsersetTree{
   682  					Root: &openfgav1.UsersetTree_Node{
   683  						Name: "group:1#viewer",
   684  						Value: &openfgav1.UsersetTree_Node_Leaf{
   685  							Leaf: &openfgav1.UsersetTree_Leaf{
   686  								Value: &openfgav1.UsersetTree_Leaf_Users{
   687  									Users: &openfgav1.UsersetTree_Users{
   688  										Users: []string{"group:2#viewer"},
   689  									},
   690  								},
   691  							},
   692  						},
   693  					},
   694  				},
   695  			},
   696  		},
   697  		{
   698  			name: "nested_groups_I",
   699  			model: testutils.MustTransformDSLToProtoWithID(`
   700  			model
   701  			  schema 1.1
   702  			
   703  			type employee
   704  			  relations
   705  				define can_manage: manager or can_manage from manager
   706  				define manager: [employee]
   707  			
   708  			type report
   709  			  relations
   710  				define approver: can_manage from submitter
   711  				define submitter: [employee]
   712  			`),
   713  			tuples: []*openfgav1.TupleKey{
   714  				// employee:d has no manager
   715  				tuple.NewTupleKey("employee:c", "manager", "employee:d"),
   716  				tuple.NewTupleKey("employee:b", "manager", "employee:c"),
   717  				tuple.NewTupleKey("employee:a", "manager", "employee:b"),
   718  			},
   719  			request: &openfgav1.ExpandRequest{
   720  				TupleKey: tuple.NewExpandRequestTupleKey("employee:d", "manager"),
   721  			},
   722  			expected: &openfgav1.ExpandResponse{
   723  				Tree: &openfgav1.UsersetTree{
   724  					Root: &openfgav1.UsersetTree_Node{
   725  						Name: "employee:d#manager",
   726  						Value: &openfgav1.UsersetTree_Node_Leaf{
   727  							Leaf: &openfgav1.UsersetTree_Leaf{
   728  								Value: &openfgav1.UsersetTree_Leaf_Users{
   729  									Users: &openfgav1.UsersetTree_Users{
   730  										// employee:d has no manager
   731  										Users: []string{},
   732  									},
   733  								},
   734  							},
   735  						},
   736  					},
   737  				},
   738  			},
   739  		},
   740  		{
   741  			name: "nested_groups_II",
   742  			model: testutils.MustTransformDSLToProtoWithID(`
   743  			model
   744  			  schema 1.1
   745  			
   746  			type employee
   747  			  relations
   748  				define can_manage: manager or can_manage from manager
   749  				define manager: [employee]
   750  			
   751  			type report
   752  			  relations
   753  				define approver: can_manage from submitter
   754  				define submitter: [employee]
   755  			`),
   756  			tuples: []*openfgav1.TupleKey{
   757  				tuple.NewTupleKey("employee:c", "manager", "employee:d"),
   758  				tuple.NewTupleKey("employee:b", "manager", "employee:c"),
   759  				tuple.NewTupleKey("employee:a", "manager", "employee:b"),
   760  			},
   761  			request: &openfgav1.ExpandRequest{
   762  				TupleKey: tuple.NewExpandRequestTupleKey("employee:c", "manager"),
   763  			},
   764  			expected: &openfgav1.ExpandResponse{
   765  				Tree: &openfgav1.UsersetTree{
   766  					Root: &openfgav1.UsersetTree_Node{
   767  						Name: "employee:c#manager",
   768  						Value: &openfgav1.UsersetTree_Node_Leaf{
   769  							Leaf: &openfgav1.UsersetTree_Leaf{
   770  								Value: &openfgav1.UsersetTree_Leaf_Users{
   771  									Users: &openfgav1.UsersetTree_Users{
   772  										Users: []string{"employee:d"},
   773  									},
   774  								},
   775  							},
   776  						},
   777  					},
   778  				},
   779  			},
   780  		},
   781  	}
   782  
   783  	ctx := context.Background()
   784  
   785  	for _, test := range tests {
   786  		t.Run(test.name, func(t *testing.T) {
   787  			// arrange
   788  			store := ulid.Make().String()
   789  			err := datastore.WriteAuthorizationModel(ctx, store, test.model)
   790  			require.NoError(t, err)
   791  
   792  			err = datastore.Write(
   793  				ctx,
   794  				store,
   795  				[]*openfgav1.TupleKeyWithoutCondition{},
   796  				test.tuples,
   797  			)
   798  			require.NoError(t, err)
   799  
   800  			require.NoError(t, err)
   801  			test.request.StoreId = store
   802  			test.request.AuthorizationModelId = test.model.GetId()
   803  
   804  			// act
   805  			query := commands.NewExpandQuery(datastore)
   806  			got, err := query.Execute(ctx, test.request)
   807  			require.NoError(t, err)
   808  
   809  			// assert
   810  			if diff := cmp.Diff(test.expected, got, protocmp.Transform()); diff != "" {
   811  				t.Errorf("mismatch (-want, +got):\n%s", diff)
   812  			}
   813  		})
   814  	}
   815  }
   816  
   817  func TestExpandQueryErrors(t *testing.T, datastore storage.OpenFGADatastore) {
   818  	tests := []struct {
   819  		name          string
   820  		model         *openfgav1.AuthorizationModel
   821  		tuples        []*openfgav1.TupleKey
   822  		request       *openfgav1.ExpandRequest
   823  		allowSchema10 bool
   824  		expected      error
   825  	}{
   826  		{
   827  			name: "missing_object_in_request",
   828  			request: &openfgav1.ExpandRequest{
   829  				TupleKey: &openfgav1.ExpandRequestTupleKey{
   830  					Relation: "bar",
   831  				},
   832  			},
   833  			model: &openfgav1.AuthorizationModel{
   834  				Id:            ulid.Make().String(),
   835  				SchemaVersion: typesystem.SchemaVersion1_1,
   836  				TypeDefinitions: []*openfgav1.TypeDefinition{
   837  					{Type: "repo"},
   838  				},
   839  			},
   840  			allowSchema10: true,
   841  			expected:      serverErrors.InvalidExpandInput,
   842  		},
   843  		{
   844  			name: "missing_object_id_and_type_in_request",
   845  			request: &openfgav1.ExpandRequest{
   846  				TupleKey: &openfgav1.ExpandRequestTupleKey{
   847  					Object:   ":",
   848  					Relation: "bar",
   849  				},
   850  			},
   851  			model: &openfgav1.AuthorizationModel{
   852  				Id:            ulid.Make().String(),
   853  				SchemaVersion: typesystem.SchemaVersion1_1,
   854  				TypeDefinitions: []*openfgav1.TypeDefinition{
   855  					{Type: "repo"},
   856  				},
   857  			},
   858  			allowSchema10: true,
   859  			expected: serverErrors.ValidationError(
   860  				fmt.Errorf("invalid 'object' field format"),
   861  			),
   862  		},
   863  		{
   864  			name: "missing_object_id_in_request",
   865  			request: &openfgav1.ExpandRequest{
   866  				TupleKey: &openfgav1.ExpandRequestTupleKey{
   867  					Object:   "github:",
   868  					Relation: "bar",
   869  				},
   870  			},
   871  			model: &openfgav1.AuthorizationModel{
   872  				Id:            ulid.Make().String(),
   873  				SchemaVersion: typesystem.SchemaVersion1_1,
   874  				TypeDefinitions: []*openfgav1.TypeDefinition{
   875  					{Type: "repo"},
   876  				},
   877  			},
   878  			allowSchema10: true,
   879  			expected: serverErrors.ValidationError(
   880  				fmt.Errorf("invalid 'object' field format"),
   881  			),
   882  		},
   883  		{
   884  			name: "missing_relation_in_request",
   885  			request: &openfgav1.ExpandRequest{
   886  				TupleKey: &openfgav1.ExpandRequestTupleKey{
   887  					Object: "bar",
   888  				},
   889  			},
   890  			model: &openfgav1.AuthorizationModel{
   891  				Id:            ulid.Make().String(),
   892  				SchemaVersion: typesystem.SchemaVersion1_1,
   893  				TypeDefinitions: []*openfgav1.TypeDefinition{
   894  					{Type: "repo"},
   895  				},
   896  			},
   897  			allowSchema10: true,
   898  			expected:      serverErrors.InvalidExpandInput,
   899  		},
   900  		{
   901  			name: "1.1_object_type_not_found_in_model",
   902  			request: &openfgav1.ExpandRequest{
   903  				TupleKey: &openfgav1.ExpandRequestTupleKey{
   904  					Object:   "foo:bar",
   905  					Relation: "baz",
   906  				},
   907  			},
   908  			model: &openfgav1.AuthorizationModel{
   909  				Id:            ulid.Make().String(),
   910  				SchemaVersion: typesystem.SchemaVersion1_1,
   911  				TypeDefinitions: []*openfgav1.TypeDefinition{
   912  					{Type: "repo"},
   913  				},
   914  			},
   915  			allowSchema10: true,
   916  			expected: serverErrors.ValidationError(
   917  				&tuple.TypeNotFoundError{TypeName: "foo"},
   918  			),
   919  		},
   920  		{
   921  			name: "1.1_relation_not_found_in_model",
   922  			model: &openfgav1.AuthorizationModel{
   923  				Id:            ulid.Make().String(),
   924  				SchemaVersion: typesystem.SchemaVersion1_1,
   925  				TypeDefinitions: []*openfgav1.TypeDefinition{
   926  					{Type: "repo"},
   927  				},
   928  			},
   929  			request: &openfgav1.ExpandRequest{
   930  				TupleKey: &openfgav1.ExpandRequestTupleKey{
   931  					Object:   "repo:bar",
   932  					Relation: "baz",
   933  				},
   934  			},
   935  			allowSchema10: true,
   936  			expected: serverErrors.ValidationError(
   937  				&tuple.RelationNotFoundError{
   938  					TypeName: "repo",
   939  					Relation: "baz",
   940  				},
   941  			),
   942  		},
   943  	}
   944  
   945  	ctx := context.Background()
   946  
   947  	for _, test := range tests {
   948  		t.Run(test.name, func(t *testing.T) {
   949  			// arrange
   950  			store := ulid.Make().String()
   951  			err := datastore.WriteAuthorizationModel(ctx, store, test.model)
   952  			require.NoError(t, err)
   953  
   954  			err = datastore.Write(
   955  				ctx,
   956  				store,
   957  				[]*openfgav1.TupleKeyWithoutCondition{},
   958  				test.tuples,
   959  			)
   960  			require.NoError(t, err)
   961  
   962  			require.NoError(t, err)
   963  			test.request.StoreId = store
   964  			test.request.AuthorizationModelId = test.model.GetId()
   965  
   966  			// act
   967  			query := commands.NewExpandQuery(datastore)
   968  			resp, err := query.Execute(ctx, test.request)
   969  
   970  			// assert
   971  			require.Nil(t, resp)
   972  			require.ErrorIs(t, err, test.expected)
   973  		})
   974  	}
   975  }