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 }