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

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/oklog/ulid/v2"
     9  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
    10  	language "github.com/openfga/language/pkg/go/transformer"
    11  	"github.com/stretchr/testify/require"
    12  	"go.uber.org/goleak"
    13  	"go.uber.org/mock/gomock"
    14  	"google.golang.org/grpc/codes"
    15  	"google.golang.org/grpc/status"
    16  
    17  	mockstorage "github.com/openfga/openfga/internal/mocks"
    18  	serverErrors "github.com/openfga/openfga/pkg/server/errors"
    19  	"github.com/openfga/openfga/pkg/storage"
    20  	"github.com/openfga/openfga/pkg/storage/memory"
    21  	"github.com/openfga/openfga/pkg/storage/test"
    22  	"github.com/openfga/openfga/pkg/testutils"
    23  	"github.com/openfga/openfga/pkg/tuple"
    24  	"github.com/openfga/openfga/pkg/typesystem"
    25  )
    26  
    27  func TestListUsersValidation(t *testing.T) {
    28  	t.Cleanup(func() {
    29  		goleak.VerifyNone(t)
    30  	})
    31  
    32  	model := `
    33  	model
    34  		schema 1.1
    35  	type user
    36  
    37  	type document
    38  		relations
    39  			define viewer: [user]`
    40  
    41  	tests := []struct {
    42  		name              string
    43  		req               *openfgav1.ListUsersRequest
    44  		model             string
    45  		expectedErrorCode codes.Code
    46  	}{
    47  		{
    48  			name: "invalid_user_filter_type",
    49  			req: &openfgav1.ListUsersRequest{
    50  				Object:   &openfgav1.Object{Type: "document", Id: "1"},
    51  				Relation: "viewer",
    52  				UserFilters: []*openfgav1.UserTypeFilter{
    53  					{
    54  						Type: "folder", // invalid type
    55  					},
    56  				},
    57  			},
    58  			model:             model,
    59  			expectedErrorCode: codes.Code(2021),
    60  		},
    61  		{
    62  			name: "invalid_user_filter_relation",
    63  			req: &openfgav1.ListUsersRequest{
    64  				Object:   &openfgav1.Object{Type: "document", Id: "1"},
    65  				Relation: "viewer",
    66  				UserFilters: []*openfgav1.UserTypeFilter{
    67  					{
    68  						Type:     "user",
    69  						Relation: "editor", // invalid relation
    70  					},
    71  				},
    72  			},
    73  			model:             model,
    74  			expectedErrorCode: codes.Code(2022),
    75  		},
    76  		{
    77  			name: "invalid_target_object_type",
    78  			req: &openfgav1.ListUsersRequest{
    79  				Object: &openfgav1.Object{
    80  					Type: "folder", // invalid type
    81  					Id:   "1",
    82  				},
    83  				Relation: "viewer",
    84  				UserFilters: []*openfgav1.UserTypeFilter{
    85  					{
    86  						Type: "user",
    87  					},
    88  				},
    89  			},
    90  			model:             model,
    91  			expectedErrorCode: codes.Code(2021),
    92  		},
    93  		{
    94  			name: "invalid_relation",
    95  			req: &openfgav1.ListUsersRequest{
    96  				Object:   &openfgav1.Object{Type: "document", Id: "1"},
    97  				Relation: "owner", // invalid relation
    98  				UserFilters: []*openfgav1.UserTypeFilter{
    99  					{
   100  						Type: "user",
   101  					},
   102  				},
   103  			},
   104  			model:             model,
   105  			expectedErrorCode: codes.Code(2022),
   106  		},
   107  		{
   108  			name: "contextual_tuple_invalid_object_type",
   109  			req: &openfgav1.ListUsersRequest{
   110  				Object:      &openfgav1.Object{Type: "document", Id: "1"},
   111  				Relation:    "viewer",
   112  				UserFilters: []*openfgav1.UserTypeFilter{{Type: "user"}},
   113  				ContextualTuples: []*openfgav1.TupleKey{
   114  					tuple.NewTupleKey("invalid_object_type:1", "viewer", "user:will"),
   115  				},
   116  			},
   117  			model:             model,
   118  			expectedErrorCode: codes.Code(2027),
   119  		},
   120  		{
   121  			name: "contextual_tuple_invalid_user_type",
   122  			req: &openfgav1.ListUsersRequest{
   123  				Object:      &openfgav1.Object{Type: "document", Id: "1"},
   124  				Relation:    "viewer",
   125  				UserFilters: []*openfgav1.UserTypeFilter{{Type: "user"}},
   126  				ContextualTuples: []*openfgav1.TupleKey{
   127  					tuple.NewTupleKey("document:1", "viewer", "invalid_user_type:will"),
   128  				},
   129  			},
   130  			model:             model,
   131  			expectedErrorCode: codes.Code(2027),
   132  		},
   133  		{
   134  			name: "contextual_tuple_invalid_relation",
   135  			req: &openfgav1.ListUsersRequest{
   136  				Object:      &openfgav1.Object{Type: "document", Id: "1"},
   137  				Relation:    "viewer",
   138  				UserFilters: []*openfgav1.UserTypeFilter{{Type: "user"}},
   139  				ContextualTuples: []*openfgav1.TupleKey{
   140  					tuple.NewTupleKey("document:1", "invalid_relation", "user:will"),
   141  				},
   142  			},
   143  			model:             model,
   144  			expectedErrorCode: codes.Code(2027),
   145  		},
   146  	}
   147  
   148  	storeID := ulid.Make().String()
   149  	for _, test := range tests {
   150  		ds := memory.New()
   151  		t.Cleanup(ds.Close)
   152  		model := testutils.MustTransformDSLToProtoWithID(test.model)
   153  
   154  		t.Run(test.name, func(t *testing.T) {
   155  			typesys, err := typesystem.NewAndValidate(context.Background(), model)
   156  			require.NoError(t, err)
   157  
   158  			err = ds.WriteAuthorizationModel(context.Background(), storeID, model)
   159  			require.NoError(t, err)
   160  
   161  			s := MustNewServerWithOpts(
   162  				WithDatastore(ds),
   163  				WithExperimentals(ExperimentalEnableListUsers),
   164  			)
   165  			t.Cleanup(s.Close)
   166  
   167  			ctx := typesystem.ContextWithTypesystem(context.Background(), typesys)
   168  
   169  			test.req.AuthorizationModelId = model.GetId()
   170  			test.req.StoreId = storeID
   171  
   172  			_, err = s.ListUsers(ctx, test.req)
   173  			e, ok := status.FromError(err)
   174  			require.True(t, ok)
   175  			require.Equal(t, test.expectedErrorCode, e.Code())
   176  		})
   177  	}
   178  }
   179  
   180  func TestModelIdNotFound(t *testing.T) {
   181  	ctx := context.Background()
   182  
   183  	req := &openfgav1.ListUsersRequest{
   184  		StoreId: "some-store-id",
   185  	}
   186  
   187  	mockController := gomock.NewController(t)
   188  	defer mockController.Finish()
   189  
   190  	mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController)
   191  	mockDatastore.EXPECT().FindLatestAuthorizationModel(gomock.Any(), gomock.Any()).Return(nil, storage.ErrNotFound)
   192  
   193  	server := MustNewServerWithOpts(
   194  		WithDatastore(mockDatastore),
   195  		WithExperimentals(ExperimentalEnableListUsers),
   196  	)
   197  	t.Cleanup(server.Close)
   198  
   199  	resp, err := server.ListUsers(ctx, req)
   200  	require.Nil(t, resp)
   201  	require.Error(t, err)
   202  
   203  	e, ok := status.FromError(err)
   204  	require.True(t, ok)
   205  	require.Equal(t, codes.Code(2020), e.Code())
   206  }
   207  
   208  func TestExperimentalListUsers(t *testing.T) {
   209  	ctx := context.Background()
   210  
   211  	req := &openfgav1.ListUsersRequest{}
   212  
   213  	mockController := gomock.NewController(t)
   214  	defer mockController.Finish()
   215  
   216  	mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController)
   217  	mockDatastore.EXPECT().FindLatestAuthorizationModel(gomock.Any(), gomock.Any()).Return(nil, storage.ErrNotFound) // error demonstrates that main code path is reached
   218  
   219  	server := MustNewServerWithOpts(
   220  		WithDatastore(mockDatastore),
   221  	)
   222  	t.Cleanup(server.Close)
   223  
   224  	t.Run("list_users_errors_if_not_experimentally_enabled", func(t *testing.T) {
   225  		_, err := server.ListUsers(ctx, req)
   226  		require.Error(t, err)
   227  		require.Equal(t, "rpc error: code = Unimplemented desc = ListUsers is not enabled. It can be enabled for experimental use by passing the `--experimentals enable-list-users` configuration option when running OpenFGA server", err.Error())
   228  
   229  		e, ok := status.FromError(err)
   230  		require.True(t, ok)
   231  		require.Equal(t, codes.Unimplemented, e.Code())
   232  	})
   233  
   234  	t.Run("list_users_does_not_error_if_experimentally_enabled", func(t *testing.T) {
   235  		server.experimentals = []ExperimentalFeatureFlag{ExperimentalEnableListUsers}
   236  		_, err := server.ListUsers(ctx, req)
   237  
   238  		require.Error(t, err)
   239  		require.Equal(t, "rpc error: code = Code(2020) desc = No authorization models found for store ''", err.Error())
   240  	})
   241  }
   242  
   243  func TestListUsers_ErrorCases(t *testing.T) {
   244  	t.Cleanup(func() {
   245  		goleak.VerifyNone(t)
   246  	})
   247  
   248  	ctx := context.Background()
   249  	store := ulid.Make().String()
   250  
   251  	t.Run("graph_resolution_errors", func(t *testing.T) {
   252  		s := MustNewServerWithOpts(
   253  			WithDatastore(memory.New()),
   254  			WithResolveNodeLimit(2),
   255  			WithExperimentals(ExperimentalEnableListUsers),
   256  		)
   257  		t.Cleanup(s.Close)
   258  
   259  		writeModelResp, err := s.WriteAuthorizationModel(ctx, &openfgav1.WriteAuthorizationModelRequest{
   260  			StoreId:       store,
   261  			SchemaVersion: typesystem.SchemaVersion1_1,
   262  			TypeDefinitions: language.MustTransformDSLToProto(`model
   263    schema 1.1
   264  type user
   265  
   266  type group
   267    relations
   268  	define member: [user, group#member]
   269  
   270  type document
   271    relations
   272  	define viewer: [group#member]`).GetTypeDefinitions(),
   273  		})
   274  		require.NoError(t, err)
   275  
   276  		_, err = s.Write(ctx, &openfgav1.WriteRequest{
   277  			StoreId: store,
   278  			Writes: &openfgav1.WriteRequestWrites{
   279  				TupleKeys: []*openfgav1.TupleKey{
   280  					tuple.NewTupleKey("document:1", "viewer", "group:1#member"),
   281  					tuple.NewTupleKey("group:1", "member", "group:2#member"),
   282  					tuple.NewTupleKey("group:2", "member", "group:3#member"),
   283  					tuple.NewTupleKey("group:3", "member", "user:jon"),
   284  				},
   285  			},
   286  		})
   287  		require.NoError(t, err)
   288  
   289  		t.Run("resolution_depth_exceeded_error_unary", func(t *testing.T) {
   290  			res, err := s.ListUsers(ctx, &openfgav1.ListUsersRequest{
   291  				StoreId:              store,
   292  				AuthorizationModelId: writeModelResp.GetAuthorizationModelId(),
   293  				Relation:             "viewer",
   294  				Object: &openfgav1.Object{
   295  					Type: "document",
   296  					Id:   "1",
   297  				},
   298  				UserFilters: []*openfgav1.UserTypeFilter{{Type: "user"}},
   299  			})
   300  
   301  			require.Nil(t, res)
   302  			require.ErrorIs(t, err, serverErrors.AuthorizationModelResolutionTooComplex)
   303  		})
   304  	})
   305  }
   306  
   307  func TestListUsers_Deadline(t *testing.T) {
   308  	t.Cleanup(func() {
   309  		goleak.VerifyNone(t)
   310  	})
   311  
   312  	ctx := context.Background()
   313  
   314  	t.Run("return_no_error_and_partial_results_at_deadline", func(t *testing.T) {
   315  		ds := memory.New()
   316  		t.Cleanup(ds.Close)
   317  
   318  		modelStr := `
   319  		model
   320  		schema 1.1
   321  		type user
   322  
   323  		type group
   324  		relations
   325  			define member: [user]
   326  
   327  		type document
   328  		relations
   329  			define viewer: [user, group#member]`
   330  
   331  		tuples := []string{
   332  			"document:1#viewer@user:jon",
   333  			"document:1#viewer@group:fga#member",
   334  			"group:fga#member@user:maria",
   335  		}
   336  
   337  		storeID, model := test.BootstrapFGAStore(t, ds, modelStr, tuples)
   338  
   339  		ds = mockstorage.NewMockSlowDataStorage(ds, 20*time.Millisecond)
   340  		t.Cleanup(ds.Close)
   341  
   342  		s := MustNewServerWithOpts(
   343  			WithDatastore(ds),
   344  			WithExperimentals(ExperimentalEnableListUsers),
   345  			WithListUsersDeadline(30*time.Millisecond), // 30ms is enough for first read, but not others
   346  		)
   347  		t.Cleanup(s.Close)
   348  
   349  		resp, err := s.ListUsers(ctx, &openfgav1.ListUsersRequest{
   350  			StoreId:              storeID,
   351  			AuthorizationModelId: model.GetId(),
   352  			Object: &openfgav1.Object{
   353  				Type: "document",
   354  				Id:   "1",
   355  			},
   356  			Relation: "viewer",
   357  			UserFilters: []*openfgav1.UserTypeFilter{
   358  				{Type: "user"},
   359  			},
   360  		})
   361  		require.NoError(t, err)
   362  		require.NotNil(t, resp)
   363  		require.Len(t, resp.GetUsers(), 1)
   364  	})
   365  
   366  	t.Run("internal_error_without_meeting_deadline", func(t *testing.T) {
   367  		mockController := gomock.NewController(t)
   368  		t.Cleanup(mockController.Finish)
   369  
   370  		mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController)
   371  
   372  		storeID := ulid.Make().String()
   373  		modelID := ulid.Make().String()
   374  
   375  		mockDatastore.EXPECT().
   376  			ReadAuthorizationModel(gomock.Any(), storeID, modelID).
   377  			Return(
   378  				testutils.MustTransformDSLToProtoWithID(`
   379  				model
   380  				  schema 1.1
   381  				type user
   382  
   383  				type document
   384  				  relations
   385  				    define viewer: [user]`),
   386  				nil,
   387  			).
   388  			Times(1)
   389  
   390  		mockDatastore.EXPECT().
   391  			Read(gomock.Any(), storeID, gomock.Any()).
   392  			Return(nil, context.DeadlineExceeded).
   393  			Times(1)
   394  
   395  		s := MustNewServerWithOpts(
   396  			WithDatastore(mockDatastore),
   397  			WithExperimentals(ExperimentalEnableListUsers),
   398  			WithListUsersDeadline(1*time.Minute),
   399  		)
   400  		t.Cleanup(s.Close)
   401  
   402  		resp, err := s.ListUsers(ctx, &openfgav1.ListUsersRequest{
   403  			StoreId:              storeID,
   404  			AuthorizationModelId: modelID,
   405  			Object: &openfgav1.Object{
   406  				Type: "document",
   407  				Id:   "1",
   408  			},
   409  			Relation: "viewer",
   410  			UserFilters: []*openfgav1.UserTypeFilter{
   411  				{Type: "user"},
   412  			},
   413  		})
   414  		require.Nil(t, resp)
   415  
   416  		st, ok := status.FromError(err)
   417  		require.True(t, ok)
   418  		require.Equal(t, codes.Code(openfgav1.InternalErrorCode_internal_error), st.Code())
   419  	})
   420  
   421  	t.Run("internal_storage_error_after_deadline", func(t *testing.T) {
   422  		mockController := gomock.NewController(t)
   423  		t.Cleanup(mockController.Finish)
   424  
   425  		mockDatastore := mockstorage.NewMockOpenFGADatastore(mockController)
   426  
   427  		storeID := ulid.Make().String()
   428  		modelID := ulid.Make().String()
   429  
   430  		mockDatastore.EXPECT().
   431  			ReadAuthorizationModel(gomock.Any(), storeID, modelID).
   432  			Return(
   433  				testutils.MustTransformDSLToProtoWithID(`
   434  				model
   435  				  schema 1.1
   436  				type user
   437  
   438  				type document
   439  				  relations
   440  				    define viewer: [user]`),
   441  				nil,
   442  			).
   443  			Times(1)
   444  
   445  		mockDatastore.EXPECT().
   446  			Read(gomock.Any(), storeID, gomock.Any()).
   447  			DoAndReturn(func(ctx context.Context, storeID string, tupleKey *openfgav1.TupleKey) (storage.TupleIterator, error) {
   448  				time.Sleep(10 * time.Millisecond)
   449  				return nil, context.Canceled
   450  			}).
   451  			Times(1)
   452  
   453  		s := MustNewServerWithOpts(
   454  			WithDatastore(mockDatastore),
   455  			WithExperimentals(ExperimentalEnableListUsers),
   456  			WithListUsersDeadline(5*time.Millisecond),
   457  		)
   458  		t.Cleanup(s.Close)
   459  
   460  		resp, err := s.ListUsers(ctx, &openfgav1.ListUsersRequest{
   461  			StoreId:              storeID,
   462  			AuthorizationModelId: modelID,
   463  			Object: &openfgav1.Object{
   464  				Type: "document",
   465  				Id:   "1",
   466  			},
   467  			Relation: "viewer",
   468  			UserFilters: []*openfgav1.UserTypeFilter{
   469  				{Type: "user"},
   470  			},
   471  		})
   472  		require.NoError(t, err)
   473  		require.NotNil(t, resp)
   474  		require.Empty(t, resp.GetUsers())
   475  	})
   476  }