github.com/openfga/openfga@v1.5.4-rc1/pkg/server/test/read.go (about) 1 package test 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/google/go-cmp/cmp" 8 "github.com/oklog/ulid/v2" 9 openfgav1 "github.com/openfga/api/proto/openfga/v1" 10 parser "github.com/openfga/language/pkg/go/transformer" 11 "github.com/stretchr/testify/require" 12 "google.golang.org/protobuf/protoadapt" 13 "google.golang.org/protobuf/testing/protocmp" 14 "google.golang.org/protobuf/types/known/wrapperspb" 15 16 "github.com/openfga/openfga/pkg/server/commands" 17 serverErrors "github.com/openfga/openfga/pkg/server/errors" 18 "github.com/openfga/openfga/pkg/storage" 19 "github.com/openfga/openfga/pkg/testutils" 20 "github.com/openfga/openfga/pkg/tuple" 21 "github.com/openfga/openfga/pkg/typesystem" 22 ) 23 24 // Read Command delegates to [storage.ReadPage]. 25 // TODO Tests here shouldn't assert on correctness of results because that should be tested in pkg/storage/test. 26 // We should pass a mock datastore and assert that mock.ReadPage was called 27 28 func ReadQuerySuccessTest(t *testing.T, datastore storage.OpenFGADatastore) { 29 // TODO: review which of these tests should be moved to validation/types in grpc rather than execution. e.g.: invalid relation in authorizationmodel is fine, but tuple without authorizationmodel is should be required before. see issue: https://github.com/openfga/sandcastle/issues/13 30 tests := []struct { 31 _name string 32 model *openfgav1.AuthorizationModel 33 tuples []*openfgav1.TupleKey 34 request *openfgav1.ReadRequest 35 response *openfgav1.ReadResponse 36 }{ 37 // { 38 // _name: "ExecuteReturnsExactMatchingTupleKey", 39 // // state 40 // model: &openfgav1.AuthorizationModel{ 41 // Id: ulid.Make().String(), 42 // SchemaVersion: typesystem.SchemaVersion1_0, 43 // TypeDefinitions: parser.MustTransformDSLToProto(`model 44 // schema 1.0 45 // type user 46 // 47 // type team 48 // 49 // type repo 50 // relations 51 // define owner: [team] 52 // define admin: [user]`).GetTypeDefinitions(), 53 // }, 54 // tuples: []*openfgav1.TupleKey{ 55 // { 56 // Object: "repo:openfga/openfga", 57 // Relation: "admin", 58 // User: "user:github|jose", 59 // }, 60 // { 61 // Object: "repo:openfga/openfga", 62 // Relation: "owner", 63 // User: "team:iam", 64 // }, 65 // }, 66 // // input 67 // request: &openfgav1.ReadRequest{ 68 // TupleKey: &openfgav1.ReadRequestTupleKey{ 69 // Object: "repo:openfga/openfga", 70 // Relation: "admin", 71 // User: "user:github|jose", 72 // }, 73 // }, 74 // // output 75 // response: &openfgav1.ReadResponse{ 76 // Tuples: []*openfgav1.Tuple{ 77 // { 78 // Key: &openfgav1.TupleKey{ 79 // Object: "repo:openfga/openfga", 80 // Relation: "admin", 81 // User: "user:github|jose", 82 // }, 83 // }, 84 // }, 85 // }, 86 // }, 87 { 88 _name: "ExecuteReturnsTuplesWithProvidedUserAndObjectIdInAuthorizationModelRegardlessOfRelationIfNoRelation", 89 // state 90 model: &openfgav1.AuthorizationModel{ 91 Id: ulid.Make().String(), 92 SchemaVersion: typesystem.SchemaVersion1_1, 93 TypeDefinitions: parser.MustTransformDSLToProto(`model 94 schema 1.1 95 type user 96 97 type repo 98 relations 99 define admin: [user] 100 define owner: [user]`).GetTypeDefinitions(), 101 }, 102 tuples: []*openfgav1.TupleKey{ 103 tuple.NewTupleKey("repo:openfga/openfga", "admin", "user:github|jose"), 104 tuple.NewTupleKey("repo:openfga/openfga", "owner", "user:github|jose"), 105 }, 106 // input 107 request: &openfgav1.ReadRequest{ 108 TupleKey: &openfgav1.ReadRequestTupleKey{ 109 Object: "repo:openfga/openfga", 110 User: "user:github|jose", 111 }, 112 }, 113 // output 114 response: &openfgav1.ReadResponse{ 115 Tuples: []*openfgav1.Tuple{ 116 {Key: tuple.NewTupleKey("repo:openfga/openfga", "admin", "user:github|jose")}, 117 {Key: tuple.NewTupleKey("repo:openfga/openfga", "owner", "user:github|jose")}, 118 }, 119 }, 120 }, 121 //{ 122 // _name: "ExecuteReturnsTuplesWithProvidedUserInAuthorizationModelRegardlessOfRelationAndObjectIdIfNoRelationAndNoObjectId", 123 // // state 124 // model: &openfgav1.AuthorizationModel{ 125 // Id: ulid.Make().String(), 126 // SchemaVersion: typesystem.SchemaVersion1_0, 127 // TypeDefinitions: []*openfgav1.TypeDefinition{ 128 // { 129 // Type: "repo", 130 // Relations: map[string]*openfgav1.Userset{ 131 // "admin": {}, 132 // "writer": {}, 133 // }, 134 // }, 135 // }, 136 // }, 137 // tuples: []*openfgav1.TupleKey{ 138 // { 139 // Object: "repo:openfga/openfga", 140 // Relation: "admin", 141 // User: "github|jose", 142 // }, 143 // { 144 // Object: "repo:openfga/openfga-server", 145 // Relation: "writer", 146 // User: "github|jose", 147 // }, 148 // { 149 // Object: "org:openfga", 150 // Relation: "member", 151 // User: "github|jose", 152 // }, 153 // }, 154 // // input 155 // request: &openfgav1.ReadRequest{ 156 // TupleKey: &openfgav1.ReadRequestTupleKey{ 157 // Object: "repo:", 158 // User: "github|jose", 159 // }, 160 // }, 161 // // output 162 // response: &openfgav1.ReadResponse{ 163 // Tuples: []*openfgav1.Tuple{ 164 // {Key: &openfgav1.TupleKey{ 165 // Object: "repo:openfga/openfga", 166 // Relation: "admin", 167 // User: "github|jose", 168 // }}, 169 // {Key: &openfgav1.TupleKey{ 170 // Object: "repo:openfga/openfga-server", 171 // Relation: "writer", 172 // User: "github|jose", 173 // }}, 174 // }, 175 // }, 176 // }, 177 //{ 178 // _name: "ExecuteReturnsTuplesWithProvidedUserAndRelationInAuthorizationModelRegardlessOfObjectIdIfNoObjectId", 179 // // state 180 // model: &openfgav1.AuthorizationModel{ 181 // Id: ulid.Make().String(), 182 // SchemaVersion: typesystem.SchemaVersion1_0, 183 // TypeDefinitions: []*openfgav1.TypeDefinition{ 184 // { 185 // Type: "repo", 186 // Relations: map[string]*openfgav1.Userset{ 187 // "admin": {}, 188 // "writer": {}, 189 // }, 190 // }, 191 // }, 192 // }, 193 // tuples: []*openfgav1.TupleKey{ 194 // { 195 // Object: "repo:openfga/openfga", 196 // Relation: "admin", 197 // User: "github|jose", 198 // }, 199 // { 200 // Object: "repo:openfga/openfga-server", 201 // Relation: "writer", 202 // User: "github|jose", 203 // }, 204 // { 205 // Object: "repo:openfga/openfga-users", 206 // Relation: "writer", 207 // User: "github|jose", 208 // }, 209 // { 210 // Object: "org:openfga", 211 // Relation: "member", 212 // User: "github|jose", 213 // }, 214 // }, 215 // // input 216 // request: &openfgav1.ReadRequest{ 217 // TupleKey: &openfgav1.ReadRequestTupleKey{ 218 // Object: "repo:", 219 // Relation: "writer", 220 // User: "github|jose", 221 // }, 222 // }, 223 // // output 224 // response: &openfgav1.ReadResponse{ 225 // Tuples: []*openfgav1.Tuple{ 226 // {Key: &openfgav1.TupleKey{ 227 // Object: "repo:openfga/openfga-server", 228 // Relation: "writer", 229 // User: "github|jose", 230 // }}, 231 // {Key: &openfgav1.TupleKey{ 232 // Object: "repo:openfga/openfga-users", 233 // Relation: "writer", 234 // User: "github|jose", 235 // }}, 236 // }, 237 // }, 238 // }, 239 { 240 _name: "ExecuteReturnsTuplesWithProvidedObjectIdAndRelationInAuthorizationModelRegardlessOfUser", 241 // state 242 model: &openfgav1.AuthorizationModel{ 243 Id: ulid.Make().String(), 244 SchemaVersion: typesystem.SchemaVersion1_0, 245 TypeDefinitions: []*openfgav1.TypeDefinition{ 246 { 247 Type: "repo", 248 Relations: map[string]*openfgav1.Userset{ 249 "admin": {}, 250 "writer": {}, 251 }, 252 }, 253 }, 254 }, 255 tuples: []*openfgav1.TupleKey{ 256 { 257 Object: "repo:openfga/openfga", 258 Relation: "admin", 259 User: "github|jose", 260 }, 261 { 262 Object: "repo:openfga/openfga", 263 Relation: "admin", 264 User: "github|yenkel", 265 }, 266 { 267 Object: "repo:openfga/openfga-users", 268 Relation: "writer", 269 User: "github|jose", 270 }, 271 { 272 Object: "org:openfga", 273 Relation: "member", 274 User: "github|jose", 275 }, 276 }, 277 // input 278 request: &openfgav1.ReadRequest{ 279 TupleKey: &openfgav1.ReadRequestTupleKey{ 280 Object: "repo:openfga/openfga", 281 Relation: "admin", 282 }, 283 }, 284 // output 285 response: &openfgav1.ReadResponse{ 286 Tuples: []*openfgav1.Tuple{ 287 {Key: &openfgav1.TupleKey{ 288 Object: "repo:openfga/openfga", 289 Relation: "admin", 290 User: "github|jose", 291 }}, 292 {Key: &openfgav1.TupleKey{ 293 Object: "repo:openfga/openfga", 294 Relation: "admin", 295 User: "github|yenkel", 296 }}, 297 }, 298 }, 299 }, 300 //{ 301 // _name: "ExecuteReturnsTuplesWithProvidedObjectIdInAuthorizationModelRegardlessOfUserAndRelation", 302 // // state 303 // model: &openfgav1.AuthorizationModel{ 304 // Id: ulid.Make().String(), 305 // SchemaVersion: typesystem.SchemaVersion1_0, 306 // TypeDefinitions: []*openfgav1.TypeDefinition{ 307 // { 308 // Type: "repo", 309 // Relations: map[string]*openfgav1.Userset{ 310 // "admin": {}, 311 // "writer": {}, 312 // }, 313 // }, 314 // }, 315 // }, 316 // tuples: []*openfgav1.TupleKey{ 317 // { 318 // Object: "repo:openfga/openfga", 319 // Relation: "admin", 320 // User: "github|jose", 321 // }, 322 // { 323 // Object: "repo:openfga/openfga", 324 // Relation: "writer", 325 // User: "github|yenkel", 326 // }, 327 // { 328 // Object: "repo:openfga/openfga-users", 329 // Relation: "writer", 330 // User: "github|jose", 331 // }, 332 // { 333 // Object: "org:openfga", 334 // Relation: "member", 335 // User: "github|jose", 336 // }, 337 // }, 338 // // input 339 // request: &openfgav1.ReadRequest{ 340 // TupleKey: &openfgav1.ReadRequestTupleKey{ 341 // Object: "repo:openfga/openfga", 342 // }, 343 // }, 344 // // output 345 // response: &openfgav1.ReadResponse{ 346 // Tuples: []*openfgav1.Tuple{ 347 // {Key: &openfgav1.TupleKey{ 348 // Object: "repo:openfga/openfga", 349 // Relation: "admin", 350 // User: "github|jose", 351 // }}, 352 // {Key: &openfgav1.TupleKey{ 353 // Object: "repo:openfga/openfga", 354 // Relation: "writer", 355 // User: "github|yenkel", 356 // }}, 357 // }, 358 // }, 359 // }, 360 } 361 362 ctx := context.Background() 363 364 for _, test := range tests { 365 t.Run(test._name, func(t *testing.T) { 366 store := ulid.Make().String() 367 err := datastore.WriteAuthorizationModel(ctx, store, test.model) 368 require.NoError(t, err) 369 370 if test.tuples != nil { 371 err = datastore.Write(ctx, store, []*openfgav1.TupleKeyWithoutCondition{}, test.tuples) 372 require.NoError(t, err) 373 } 374 375 test.request.StoreId = store 376 resp, err := commands.NewReadQuery(datastore).Execute(ctx, test.request) 377 require.NoError(t, err) 378 379 if test.response.GetTuples() != nil { 380 require.Equal(t, len(test.response.GetTuples()), len(resp.GetTuples())) 381 382 for i, responseTuple := range test.response.GetTuples() { 383 responseTupleKey := responseTuple.GetKey() 384 actualTupleKey := resp.GetTuples()[i].GetKey() 385 require.Equal(t, responseTupleKey.GetObject(), actualTupleKey.GetObject()) 386 require.Equal(t, responseTupleKey.GetRelation(), actualTupleKey.GetRelation()) 387 require.Equal(t, responseTupleKey.GetUser(), actualTupleKey.GetUser()) 388 } 389 } 390 }) 391 } 392 } 393 394 func ReadQueryErrorTest(t *testing.T, datastore storage.OpenFGADatastore) { 395 // TODO: review which of these tests should be moved to validation/types in grpc rather than execution. e.g.: invalid relation in authorizationmodel is fine, but tuple without authorizationmodel is should be required before. see issue: https://github.com/openfga/sandcastle/issues/13 396 tests := []struct { 397 _name string 398 model *openfgav1.AuthorizationModel 399 request *openfgav1.ReadRequest 400 }{ 401 { 402 _name: "ExecuteErrorsIfOneTupleKeyHasObjectWithoutType", 403 model: &openfgav1.AuthorizationModel{ 404 Id: ulid.Make().String(), 405 SchemaVersion: typesystem.SchemaVersion1_0, 406 TypeDefinitions: []*openfgav1.TypeDefinition{ 407 { 408 Type: "repo", 409 Relations: map[string]*openfgav1.Userset{ 410 "admin": {}, 411 }, 412 }, 413 }, 414 }, 415 request: &openfgav1.ReadRequest{ 416 TupleKey: &openfgav1.ReadRequestTupleKey{ 417 Object: "openfga/iam", 418 }, 419 }, 420 }, 421 { 422 _name: "ExecuteErrorsIfOneTupleKeyObjectIs':'", 423 model: &openfgav1.AuthorizationModel{ 424 Id: ulid.Make().String(), 425 SchemaVersion: typesystem.SchemaVersion1_0, 426 TypeDefinitions: []*openfgav1.TypeDefinition{ 427 { 428 Type: "repo", 429 Relations: map[string]*openfgav1.Userset{ 430 "admin": {}, 431 }, 432 }, 433 }, 434 }, 435 request: &openfgav1.ReadRequest{ 436 TupleKey: &openfgav1.ReadRequestTupleKey{ 437 Object: ":", 438 }, 439 }, 440 }, 441 { 442 _name: "ErrorIfRequestHasNoObjectAndThusNoType", 443 model: &openfgav1.AuthorizationModel{ 444 Id: ulid.Make().String(), 445 SchemaVersion: typesystem.SchemaVersion1_0, 446 TypeDefinitions: []*openfgav1.TypeDefinition{ 447 { 448 Type: "repo", 449 Relations: map[string]*openfgav1.Userset{ 450 "admin": {}, 451 }, 452 }, 453 }, 454 }, 455 request: &openfgav1.ReadRequest{ 456 TupleKey: &openfgav1.ReadRequestTupleKey{ 457 Relation: "admin", 458 User: "github|jonallie", 459 }, 460 }, 461 }, 462 { 463 _name: "ExecuteErrorsIfOneTupleKeyHasNoObjectIdAndNoUserSetButHasAType", 464 model: &openfgav1.AuthorizationModel{ 465 Id: ulid.Make().String(), 466 SchemaVersion: typesystem.SchemaVersion1_0, 467 TypeDefinitions: []*openfgav1.TypeDefinition{ 468 { 469 Type: "repo", 470 Relations: map[string]*openfgav1.Userset{ 471 "admin": {}, 472 }, 473 }, 474 }, 475 }, 476 request: &openfgav1.ReadRequest{ 477 TupleKey: &openfgav1.ReadRequestTupleKey{ 478 Object: "repo:", 479 Relation: "writer", 480 }, 481 }, 482 }, 483 { 484 _name: "ExecuteErrorsIfOneTupleKeyInTupleSetOnlyHasRelation", 485 model: &openfgav1.AuthorizationModel{ 486 Id: ulid.Make().String(), 487 SchemaVersion: typesystem.SchemaVersion1_0, 488 TypeDefinitions: []*openfgav1.TypeDefinition{ 489 { 490 Type: "repo", 491 Relations: map[string]*openfgav1.Userset{ 492 "admin": {}, 493 }, 494 }, 495 }, 496 }, 497 request: &openfgav1.ReadRequest{ 498 TupleKey: &openfgav1.ReadRequestTupleKey{ 499 Relation: "writer", 500 }, 501 }, 502 }, 503 { 504 _name: "ExecuteErrorsIfContinuationTokenIsBad", 505 model: &openfgav1.AuthorizationModel{ 506 Id: ulid.Make().String(), 507 SchemaVersion: typesystem.SchemaVersion1_0, 508 TypeDefinitions: []*openfgav1.TypeDefinition{ 509 { 510 Type: "repo", 511 Relations: map[string]*openfgav1.Userset{ 512 "admin": {}, 513 "writer": {}, 514 }, 515 }, 516 }, 517 }, 518 request: &openfgav1.ReadRequest{ 519 TupleKey: &openfgav1.ReadRequestTupleKey{ 520 Object: "repo:openfga/openfga", 521 }, 522 ContinuationToken: "foo", 523 }, 524 }, 525 } 526 527 ctx := context.Background() 528 529 for _, test := range tests { 530 t.Run(test._name, func(t *testing.T) { 531 store := ulid.Make().String() 532 err := datastore.WriteAuthorizationModel(ctx, store, test.model) 533 require.NoError(t, err) 534 535 test.request.StoreId = store 536 _, err = commands.NewReadQuery(datastore).Execute(ctx, test.request) 537 require.Error(t, err) 538 }) 539 } 540 } 541 542 func ReadAllTuplesTest(t *testing.T, datastore storage.OpenFGADatastore) { 543 ctx := context.Background() 544 store := ulid.Make().String() 545 546 writes := []*openfgav1.TupleKey{ 547 { 548 Object: "repo:openfga/foo", 549 Relation: "admin", 550 User: "github|jon.allie", 551 }, 552 { 553 Object: "repo:openfga/bar", 554 Relation: "admin", 555 User: "github|jon.allie", 556 }, 557 { 558 Object: "repo:openfga/baz", 559 Relation: "admin", 560 User: "github|jon.allie", 561 }, 562 } 563 err := datastore.Write(ctx, store, nil, writes) 564 require.NoError(t, err) 565 566 cmd := commands.NewReadQuery(datastore) 567 568 firstRequest := &openfgav1.ReadRequest{ 569 StoreId: store, 570 PageSize: wrapperspb.Int32(1), 571 ContinuationToken: "", 572 } 573 firstResponse, err := cmd.Execute(ctx, firstRequest) 574 require.NoError(t, err) 575 576 require.Len(t, firstResponse.GetTuples(), 1) 577 require.NotEmpty(t, firstResponse.GetContinuationToken()) 578 579 var receivedTuples []*openfgav1.TupleKey 580 for _, tuple := range firstResponse.GetTuples() { 581 receivedTuples = append(receivedTuples, tuple.GetKey()) 582 } 583 584 secondRequest := &openfgav1.ReadRequest{StoreId: store, ContinuationToken: firstResponse.GetContinuationToken()} 585 secondResponse, err := cmd.Execute(ctx, secondRequest) 586 require.NoError(t, err) 587 588 require.Len(t, secondResponse.GetTuples(), 2) 589 require.Empty(t, secondResponse.GetContinuationToken()) 590 591 for _, tuple := range secondResponse.GetTuples() { 592 receivedTuples = append(receivedTuples, tuple.GetKey()) 593 } 594 595 cmpOpts := []cmp.Option{ 596 protocmp.IgnoreFields(protoadapt.MessageV2Of(&openfgav1.Tuple{}), "timestamp"), 597 protocmp.IgnoreFields(protoadapt.MessageV2Of(&openfgav1.TupleChange{}), "timestamp"), 598 protocmp.Transform(), 599 testutils.TupleKeyCmpTransformer, 600 } 601 602 if diff := cmp.Diff(writes, receivedTuples, cmpOpts...); diff != "" { 603 t.Errorf("Tuple mismatch (-want +got):\n%s", diff) 604 } 605 } 606 607 func ReadAllTuplesInvalidContinuationTokenTest(t *testing.T, datastore storage.OpenFGADatastore) { 608 ctx := context.Background() 609 store := ulid.Make().String() 610 611 model := &openfgav1.AuthorizationModel{ 612 Id: ulid.Make().String(), 613 SchemaVersion: typesystem.SchemaVersion1_0, 614 TypeDefinitions: []*openfgav1.TypeDefinition{ 615 { 616 Type: "repo", 617 }, 618 }, 619 } 620 621 err := datastore.WriteAuthorizationModel(ctx, store, model) 622 require.NoError(t, err) 623 624 _, err = commands.NewReadQuery(datastore).Execute(ctx, &openfgav1.ReadRequest{ 625 StoreId: store, 626 ContinuationToken: "foo", 627 }) 628 require.ErrorIs(t, err, serverErrors.InvalidContinuationToken) 629 }