From f4021d5c01420aeb5ab0799ef31d70a42269267d Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Fri, 30 Aug 2024 11:24:08 +0100 Subject: [PATCH 1/5] feat(query): support usage of consistency parameter Closes #379 --- README.md | 13 ++- cmd/query/check.go | 14 ++- cmd/query/check_test.go | 78 ++++++++++++- cmd/query/expand.go | 23 +++- cmd/query/expand_test.go | 61 +++++++++- cmd/query/list-objects.go | 15 ++- cmd/query/list-objects_test.go | 75 ++++++++++++- cmd/query/list-relations.go | 22 +++- cmd/query/list-relations_test.go | 130 ++++++++++++++++++++-- cmd/query/list-users.go | 13 ++- cmd/query/list-users_test.go | 89 ++++++++++++++- cmd/query/query.go | 5 + internal/cmdutils/get-consistency.go | 55 +++++++++ internal/cmdutils/get-consistency_test.go | 74 ++++++++++++ 14 files changed, 636 insertions(+), 31 deletions(-) create mode 100644 internal/cmdutils/get-consistency.go create mode 100644 internal/cmdutils/get-consistency_test.go diff --git a/README.md b/README.md index 1459f91..cc450a3 100644 --- a/README.md +++ b/README.md @@ -981,10 +981,11 @@ fga query **check** [--condition] [--contextual-tuple * `--model-id`: Specifies the model id to target (optional) * `--contextual-tuple`: Contextual tuples (optional) * `--context`: Condition context (optional) +* `--consistency`: Consistency preference (optional) ###### Example - `fga query check --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap --contextual-tuple "user:anne can_view folder:product" --contextual-tuple "folder:product parent document:roadmap"` -- `fga query check --store-id="01H4P8Z95KTXXEP6Z03T75Q984" user:anne can_view document:roadmap --context '{"ip_address":"127.0.0.1"}'` +- `fga query check --store-id="01H4P8Z95KTXXEP6Z03T75Q984" user:anne can_view document:roadmap --context '{"ip_address":"127.0.0.1"}' --consistency="HIGHER_CONSISTENCY"` ###### Response @@ -1004,10 +1005,11 @@ fga query **list-objects** [--contextual-tuple " * `--model-id`: Specifies the model id to target (optional) * `--contextual-tuple`: Contextual tuples (optional) (can be multiple) * `--context`: Condition context (optional) +* `--consistency`: Consistency preference (optional) ###### Example - `fga query list-objects --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document --contextual-tuple "user:anne can_view folder:product" --contextual-tuple "folder:product parent document:roadmap"` -- `fga query list-objects --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document --context '{"ip_address":"127.0.0.1"}` +- `fga query list-objects --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document --context '{"ip_address":"127.0.0.1"} --consistency="HIGHER_CONSISTENCY"` ###### Response ```json5 @@ -1029,11 +1031,12 @@ fga query **list-relations** [--relation ]* [--context * `--model-id`: Specifies the model id to target (optional) * `--contextual-tuple`: Contextual tuples (optional) (can be multiple) * `--context`: Condition context (optional) +* `--consistency`: Consistency preference (optional) ###### Example - `fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap --relation can_view` - `fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap --relation can_view --contextual-tuple "user:anne can_view folder:product"` -- `fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap --relation can_view --context '{"ip_address":"127.0.0.1"}` +- `fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap --relation can_view --context '{"ip_address":"127.0.0.1"} --consistency="HIGHER_CONSISTENCY"` ###### Response ```json5 @@ -1052,6 +1055,7 @@ fga query **expand** --store-id= [--model-id= --relation --user-filter < * `--model-id`: Specifies the model id to target (optional) * `--contextual-tuple`: Contextual tuples (optional) (can be multiple) * `--context`: Condition context (optional) +* `--consistency`: Consistency preference (optional) ###### Example - `fga query list-users --store-id=01H0H015178Y2V4CX10C2KGHF4 --object document:roadmap --relation can_view --user-filter user` - `fga query list-users --store-id=01H0H015178Y2V4CX10C2KGHF4 --object document:roadmap --relation can_view --user-filter user --contextual-tuple "user:anne can_view folder:product"` -- `fga query list-users --store-id=01H0H015178Y2V4CX10C2KGHF4 --object document:roadmap --relation can_view --user-filter group#member --context '{"ip_address":"127.0.0.1"}` +- `fga query list-users --store-id=01H0H015178Y2V4CX10C2KGHF4 --object document:roadmap --relation can_view --user-filter group#member --context '{"ip_address":"127.0.0.1"} --consistency="HIGHER_CONSISTENCY"` ###### Response ```json5 diff --git a/cmd/query/check.go b/cmd/query/check.go index 65cd5c8..78f3a9e 100644 --- a/cmd/query/check.go +++ b/cmd/query/check.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "github.com/spf13/cobra" @@ -34,6 +35,7 @@ func check( object string, contextualTuples []client.ClientContextualTupleKey, queryContext *map[string]interface{}, + consistency *openfga.ConsistencyPreference, ) (*client.ClientCheckResponse, error) { body := &client.ClientCheckRequest{ User: user, @@ -44,6 +46,11 @@ func check( } options := &client.ClientCheckOptions{} + // Don't set if UNSPECIFIED has been provided, it's the default anyway + if *consistency != openfga.CONSISTENCYPREFERENCE_UNSPECIFIED { + options.Consistency = consistency + } + response, err := fgaClient.Check(context.Background()).Body(*body).Options(*options).Execute() if err != nil { return nil, fmt.Errorf("failed to check due to %w", err) @@ -76,7 +83,12 @@ var checkCmd = &cobra.Command{ return fmt.Errorf("error parsing query context for check: %w", err) } - response, err := check(fgaClient, args[0], args[1], args[2], contextualTuples, queryContext) + consistency, err := cmdutils.ParseConsistencyFromCmd(cmd) + if err != nil { + return fmt.Errorf("error parsing consistency for check: %w", err) + } + + response, err := check(fgaClient, args[0], args[1], args[2], contextualTuples, queryContext, consistency) if err != nil { return fmt.Errorf("failed to check due to %w", err) } diff --git a/cmd/query/check_test.go b/cmd/query/check_test.go index 76ee480..f4c8bbf 100644 --- a/cmd/query/check_test.go +++ b/cmd/query/check_test.go @@ -47,7 +47,15 @@ func TestCheckWithError(t *testing.T) { mockFgaClient.EXPECT().Check(context.Background()).Return(mockBody) - _, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples, queryContext) + _, err := check( + mockFgaClient, + "user:foo", + "writer", + "doc:doc1", + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err == nil { t.Error("Expect error but there is none") } @@ -91,7 +99,73 @@ func TestCheckWithNoError(t *testing.T) { mockFgaClient.EXPECT().Check(context.Background()).Return(mockBody) - output, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples, queryContext) + output, err := check( + mockFgaClient, + "user:foo", + "writer", + "doc:doc1", + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) + if err != nil { + t.Error(err) + } + + if *output != expectedResponse { + t.Errorf("Expected output %v actual %v", expectedResponse, *output) + } +} + +func TestCheckWithConsistency(t *testing.T) { + t.Parallel() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) + + mockExecute := mock_client.NewMockSdkClientCheckRequestInterface(mockCtrl) + + expectedResponse := client.ClientCheckResponse{ + CheckResponse: openfga.CheckResponse{ + Allowed: openfga.PtrBool(true), + }, + HttpResponse: nil, + } + + mockExecute.EXPECT().Execute().Return(&expectedResponse, nil) + + mockRequest := mock_client.NewMockSdkClientCheckRequestInterface(mockCtrl) + options := client.ClientCheckOptions{ + Consistency: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + } + mockRequest.EXPECT().Options(options).Return(mockExecute) + + mockBody := mock_client.NewMockSdkClientCheckRequestInterface(mockCtrl) + + contextualTuples := []client.ClientContextualTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } + body := client.ClientCheckRequest{ + User: "user:foo", + Relation: "writer", + Object: "doc:doc1", + ContextualTuples: contextualTuples, + Context: queryContext, + } + mockBody.EXPECT().Body(body).Return(mockRequest) + + mockFgaClient.EXPECT().Check(context.Background()).Return(mockBody) + + output, err := check( + mockFgaClient, + "user:foo", + "writer", + "doc:doc1", + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + ) if err != nil { t.Error(err) } diff --git a/cmd/query/expand.go b/cmd/query/expand.go index d76b955..feed6ad 100644 --- a/cmd/query/expand.go +++ b/cmd/query/expand.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "github.com/spf13/cobra" @@ -27,13 +28,24 @@ import ( "github.com/openfga/cli/internal/output" ) -func expand(fgaClient client.SdkClient, relation string, object string) (*client.ClientExpandResponse, error) { +func expand( + fgaClient client.SdkClient, + relation string, + object string, + consistency *openfga.ConsistencyPreference, +) (*client.ClientExpandResponse, error) { body := &client.ClientExpandRequest{ Relation: relation, Object: object, } - tuples, err := fgaClient.Expand(context.Background()).Body(*body).Execute() + options := &client.ClientExpandOptions{} + + if *consistency != openfga.CONSISTENCYPREFERENCE_UNSPECIFIED { + options.Consistency = consistency + } + + tuples, err := fgaClient.Expand(context.Background()).Body(*body).Options(*options).Execute() if err != nil { return nil, fmt.Errorf("failed to expand tuples due to %w", err) } @@ -56,7 +68,12 @@ var expandCmd = &cobra.Command{ return fmt.Errorf("failed to initialize FGA Client due to %w", err) } - response, err := expand(fgaClient, args[0], args[1]) + consistency, err := cmdutils.ParseConsistencyFromCmd(cmd) + if err != nil { + return fmt.Errorf("error parsing consistency for check: %w", err) + } + + response, err := expand(fgaClient, args[0], args[1], consistency) if err != nil { return err } diff --git a/cmd/query/expand_test.go b/cmd/query/expand_test.go index 0c40f35..a374004 100644 --- a/cmd/query/expand_test.go +++ b/cmd/query/expand_test.go @@ -7,6 +7,7 @@ import ( "reflect" "testing" + openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "go.uber.org/mock/gomock" @@ -28,17 +29,21 @@ func TestExpandWithError(t *testing.T) { mockExecute.EXPECT().Execute().Return(&expectedResponse, errMockExpand) + mockRequest := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl) + options := client.ClientExpandOptions{} + mockRequest.EXPECT().Options(options).Return(mockExecute) + mockBody := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl) body := client.ClientExpandRequest{ Relation: "writer", Object: "doc:doc1", } - mockBody.EXPECT().Body(body).Return(mockExecute) + mockBody.EXPECT().Body(body).Return(mockRequest) mockFgaClient.EXPECT().Expand(context.Background()).Return(mockBody) - _, err := expand(mockFgaClient, "writer", "doc:doc1") + _, err := expand(mockFgaClient, "writer", "doc:doc1", openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr()) if err == nil { t.Error("Expect error but there is none") } @@ -62,17 +67,65 @@ func TestExpandWithNoError(t *testing.T) { mockExecute.EXPECT().Execute().Return(&expectedResponse, nil) + mockRequest := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl) + options := client.ClientExpandOptions{} + mockRequest.EXPECT().Options(options).Return(mockExecute) + + mockBody := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl) + + body := client.ClientExpandRequest{ + Relation: "writer", + Object: "doc:doc1", + } + mockBody.EXPECT().Body(body).Return(mockRequest) + + mockFgaClient.EXPECT().Expand(context.Background()).Return(mockBody) + + output, err := expand(mockFgaClient, "writer", "doc:doc1", openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr()) + if err != nil { + t.Error(err) + } + + if !(reflect.DeepEqual(*output, expectedResponse)) { + t.Errorf("Expect output response %v actual response %v", expandResponseTxt, *output) + } +} + +func TestExpandWithConsistency(t *testing.T) { + t.Parallel() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) + + mockExecute := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl) + + expandResponseTxt := `{"tree":{"root":{"name":"document:roadmap#viewer","union":{"nodes":[{"name": "document:roadmap#viewer","leaf":{"users":{"users":["user:81684243-9356-4421-8fbf-a4f8d36aa31b"]}}}]}}}}` //nolint:all + + expectedResponse := client.ClientExpandResponse{} + if err := json.Unmarshal([]byte(expandResponseTxt), &expectedResponse); err != nil { + t.Fatalf("%v", err) + } + + mockExecute.EXPECT().Execute().Return(&expectedResponse, nil) + + mockRequest := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl) + options := client.ClientExpandOptions{ + Consistency: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + } + mockRequest.EXPECT().Options(options).Return(mockExecute) + mockBody := mock_client.NewMockSdkClientExpandRequestInterface(mockCtrl) body := client.ClientExpandRequest{ Relation: "writer", Object: "doc:doc1", } - mockBody.EXPECT().Body(body).Return(mockExecute) + mockBody.EXPECT().Body(body).Return(mockRequest) mockFgaClient.EXPECT().Expand(context.Background()).Return(mockBody) - output, err := expand(mockFgaClient, "writer", "doc:doc1") + output, err := expand(mockFgaClient, "writer", "doc:doc1", openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr()) if err != nil { t.Error(err) } diff --git a/cmd/query/list-objects.go b/cmd/query/list-objects.go index 3e62473..c5194e0 100644 --- a/cmd/query/list-objects.go +++ b/cmd/query/list-objects.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "github.com/spf13/cobra" @@ -35,6 +36,7 @@ func listObjects( objectType string, contextualTuples []client.ClientContextualTupleKey, queryContext *map[string]interface{}, + consistency *openfga.ConsistencyPreference, ) (*client.ClientListObjectsResponse, error) { body := &client.ClientListObjectsRequest{ User: user, @@ -45,6 +47,10 @@ func listObjects( } options := &client.ClientListObjectsOptions{} + if *consistency != openfga.CONSISTENCYPREFERENCE_UNSPECIFIED { + options.Consistency = consistency + } + response, err := fgaClient.ListObjects(context.Background()).Body(*body).Options(*options).Execute() if err != nil { return nil, fmt.Errorf("failed to list objects due to %w", err) @@ -75,10 +81,15 @@ var listObjectsCmd = &cobra.Command{ queryContext, err := cmdutils.ParseQueryContext(cmd, "context") if err != nil { - return fmt.Errorf("error parsing query context for check: %w", err) + return fmt.Errorf("error parsing query context for listObjects: %w", err) + } + + consistency, err := cmdutils.ParseConsistencyFromCmd(cmd) + if err != nil { + return fmt.Errorf("error parsing consistency for listObjects: %w", err) } - response, err := listObjects(fgaClient, args[0], args[1], args[2], contextualTuples, queryContext) + response, err := listObjects(fgaClient, args[0], args[1], args[2], contextualTuples, queryContext, consistency) if err != nil { return fmt.Errorf("failed to list objects due to %w", err) } diff --git a/cmd/query/list-objects_test.go b/cmd/query/list-objects_test.go index 4f63365..186da13 100644 --- a/cmd/query/list-objects_test.go +++ b/cmd/query/list-objects_test.go @@ -5,6 +5,7 @@ import ( "errors" "testing" + openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "go.uber.org/mock/gomock" @@ -46,7 +47,15 @@ func TestListObjectsWithError(t *testing.T) { mockFgaClient.EXPECT().ListObjects(context.Background()).Return(mockBody) - _, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples, queryContext) + _, err := listObjects( + mockFgaClient, + "user:foo", + "writer", + "doc", + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err == nil { t.Error("Expect error but there is none") } @@ -86,7 +95,69 @@ func TestListObjectsWithNoError(t *testing.T) { mockFgaClient.EXPECT().ListObjects(context.Background()).Return(mockBody) - output, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples, nil) + output, err := listObjects( + mockFgaClient, + "user:foo", + "writer", + "doc", + contextualTuples, + nil, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) + if err != nil { + t.Error(err) + } + + if output != &expectedResponse { + t.Errorf("Expect %v but actual %v", expectedResponse, *output) + } +} + +func TestListObjectsWithConsistency(t *testing.T) { + t.Parallel() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) + + mockExecute := mock_client.NewMockSdkClientListObjectsRequestInterface(mockCtrl) + + expectedResponse := client.ClientListObjectsResponse{ + Objects: []string{"doc:doc1", "doc:doc2"}, + } + + mockExecute.EXPECT().Execute().Return(&expectedResponse, nil) + + mockRequest := mock_client.NewMockSdkClientListObjectsRequestInterface(mockCtrl) + options := client.ClientListObjectsOptions{ + Consistency: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + } + mockRequest.EXPECT().Options(options).Return(mockExecute) + + mockBody := mock_client.NewMockSdkClientListObjectsRequestInterface(mockCtrl) + + contextualTuples := []client.ClientContextualTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } + body := client.ClientListObjectsRequest{ + User: "user:foo", + Relation: "writer", + Type: "doc", + ContextualTuples: contextualTuples, + } + mockBody.EXPECT().Body(body).Return(mockRequest) + + mockFgaClient.EXPECT().ListObjects(context.Background()).Return(mockBody) + + output, err := listObjects( + mockFgaClient, + "user:foo", + "writer", + "doc", + contextualTuples, + nil, + openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + ) if err != nil { t.Error(err) } diff --git a/cmd/query/list-relations.go b/cmd/query/list-relations.go index 4c471af..a351cd3 100644 --- a/cmd/query/list-relations.go +++ b/cmd/query/list-relations.go @@ -78,6 +78,7 @@ func listRelations(clientConfig fga.ClientConfig, relations []string, contextualTuples []client.ClientContextualTupleKey, queryContext *map[string]interface{}, + consistency *openfga.ConsistencyPreference, ) (*client.ClientListRelationsResponse, error) { if len(relations) < 1 { relationsForType, err := getRelationsForType(clientConfig, fgaClient, object) @@ -104,6 +105,11 @@ func listRelations(clientConfig fga.ClientConfig, } options := &client.ClientListRelationsOptions{} + // Don't set if UNSPECIFIED has been provided, it's the default anyway + if *consistency != openfga.CONSISTENCYPREFERENCE_UNSPECIFIED { + options.Consistency = consistency + } + response, err := fgaClient.ListRelations(context.Background()).Body(*body).Options(*options).Execute() if err != nil { return nil, fmt.Errorf("failed to list relations due to %w", err) @@ -140,9 +146,23 @@ var listRelationsCmd = &cobra.Command{ return fmt.Errorf("error parsing query context for check: %w", err) } + consistency, err := cmdutils.ParseConsistencyFromCmd(cmd) + if err != nil { + return fmt.Errorf("error parsing consistency for check: %w", err) + } + relations, _ := cmd.Flags().GetStringArray("relation") - response, err := listRelations(clientConfig, fgaClient, args[0], args[1], relations, contextualTuples, queryContext) + response, err := listRelations( + clientConfig, + fgaClient, + args[0], + args[1], + relations, + contextualTuples, + queryContext, + consistency, + ) if err != nil { return fmt.Errorf("failed to list relations due to %w", err) } diff --git a/cmd/query/list-relations_test.go b/cmd/query/list-relations_test.go index e8afc17..c8ff384 100644 --- a/cmd/query/list-relations_test.go +++ b/cmd/query/list-relations_test.go @@ -7,6 +7,7 @@ import ( "reflect" "testing" + openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "go.uber.org/mock/gomock" @@ -41,7 +42,16 @@ func TestListRelationsLatestAuthModelError(t *testing.T) { contextualTuples := []client.ClientContextualTupleKey{ {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, } - _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples, queryContext) + _, err := listRelations( + clientConfig, + mockFgaClient, + "user:foo", + "doc:doc1", + relations, + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err == nil { t.Error("Expect error but there is none") @@ -70,7 +80,16 @@ func TestListRelationsAuthModelSpecifiedError(t *testing.T) { contextualTuples := []client.ClientContextualTupleKey{ {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, } - _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples, queryContext) + _, err := listRelations( + clientConfig, + mockFgaClient, + "user:foo", + "doc:doc1", + relations, + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err == nil { t.Error("Expect error but there is none") @@ -128,7 +147,16 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) { var clientConfig fga.ClientConfig - _, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples, queryContext) + _, err := listRelations( + clientConfig, + mockFgaClient, + "user:foo", + "doc:doc1", + relations, + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err == nil { t.Error("Expect error but there is none") } @@ -164,8 +192,16 @@ func TestListRelationsLatestAuthModelEmpty(t *testing.T) { Relations: []string{}, } - response, err := listRelations(clientConfig, mockFgaClient, "doc:doc1", "user:foo", relations, - contextualTuples, queryContext) + response, err := listRelations( + clientConfig, + mockFgaClient, + "doc:doc1", + "user:foo", + relations, + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err != nil { t.Error(err) } @@ -228,8 +264,16 @@ func TestListRelationsLatestAuthModelList(t *testing.T) { var clientConfig fga.ClientConfig - output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, - contextualTuples, queryContext) + output, err := listRelations( + clientConfig, + mockFgaClient, + "user:foo", + "doc:doc1", + relations, + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err != nil { t.Error(err) } @@ -280,8 +324,76 @@ func TestListRelationsMultipleRelations(t *testing.T) { var clientConfig fga.ClientConfig - output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, - contextualTuples, queryContext) + output, err := listRelations( + clientConfig, + mockFgaClient, + "user:foo", + "doc:doc1", + relations, + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) + if err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(*output, expectedListRelationsResponse) { + t.Errorf("Expect output %v actual %v", expectedListRelationsResponse, *output) + } +} + +func TestListRelationsWithConsistency(t *testing.T) { + t.Parallel() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) + + mockListRelationsExecute := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl) + + expectedListRelationsResponse := client.ClientListRelationsResponse{ + Relations: []string{"viewer"}, + } + + mockListRelationsExecute.EXPECT().Execute().Return(&expectedListRelationsResponse, nil) + + mockListRelationsRequest := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl) + listRelationsOptions := client.ClientListRelationsOptions{ + Consistency: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + } + mockListRelationsRequest.EXPECT().Options(listRelationsOptions).Return(mockListRelationsExecute) + + mockBody := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl) + + relations := []string{"viewer", "editor"} + contextualTuples := []client.ClientContextualTupleKey{ + {User: "user:foo", Relation: "admin", Object: "doc:doc1"}, + } + body := client.ClientListRelationsRequest{ + User: "user:foo", + Relations: []string{"viewer", "editor"}, + Object: "doc:doc1", + ContextualTuples: contextualTuples, + Context: queryContext, + } + mockBody.EXPECT().Body(body).Return(mockListRelationsRequest) + gomock.InOrder( + mockFgaClient.EXPECT().ListRelations(context.Background()).Return(mockBody), + ) + + var clientConfig fga.ClientConfig + + output, err := listRelations( + clientConfig, + mockFgaClient, + "user:foo", + "doc:doc1", + relations, + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + ) if err != nil { t.Error(err) } diff --git a/cmd/query/list-users.go b/cmd/query/list-users.go index db6b8de..84d35ac 100644 --- a/cmd/query/list-users.go +++ b/cmd/query/list-users.go @@ -66,6 +66,7 @@ func listUsers( rawUserFilter string, contextualTuples []client.ClientContextualTupleKey, queryContext *map[string]interface{}, + consistency *openfga.ConsistencyPreference, ) (*client.ClientListUsersResponse, error) { body := &client.ClientListUsersRequest{ Object: parseObject(rawObject), @@ -76,6 +77,11 @@ func listUsers( } options := &client.ClientListUsersOptions{} + // Don't set if UNSPECIFIED has been provided, it's the default anyway + if *consistency != openfga.CONSISTENCYPREFERENCE_UNSPECIFIED { + options.Consistency = consistency + } + response, err := fgaClient.ListUsers(context.Background()).Body(*body).Options(*options).Execute() if err != nil { return nil, fmt.Errorf("failed to list users due to %w", err) @@ -106,11 +112,16 @@ var listUsersCmd = &cobra.Command{ return fmt.Errorf("error parsing query context: %w", err) } + consistency, err := cmdutils.ParseConsistencyFromCmd(cmd) + if err != nil { + return fmt.Errorf("error parsing consistency for check: %w", err) + } + userFilter, _ := cmd.Flags().GetString("user-filter") object, _ := cmd.Flags().GetString("object") relation, _ := cmd.Flags().GetString("relation") - response, err := listUsers(fgaClient, object, relation, userFilter, contextualTuples, queryContext) + response, err := listUsers(fgaClient, object, relation, userFilter, contextualTuples, queryContext, consistency) if err != nil { return fmt.Errorf("failed to list users due to %w", err) } diff --git a/cmd/query/list-users_test.go b/cmd/query/list-users_test.go index bba07db..cf1ded0 100644 --- a/cmd/query/list-users_test.go +++ b/cmd/query/list-users_test.go @@ -60,7 +60,15 @@ func TestListUsersSimpleType(t *testing.T) { mockFgaClient.EXPECT().ListUsers(context.Background()).Return(mockBody) - output, err := listUsers(mockFgaClient, "doc:doc1", "admin", "user", contextualTuples, queryContext) + output, err := listUsers( + mockFgaClient, + "doc:doc1", + "admin", + "user", + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err != nil { t.Error(err) } @@ -120,7 +128,84 @@ func TestListUsersSimpleTypeAndRelation(t *testing.T) { mockFgaClient.EXPECT().ListUsers(context.Background()).Return(mockBody) - output, err := listUsers(mockFgaClient, "doc:doc1", "admin", "group#member", contextualTuples, queryContext) + output, err := listUsers( + mockFgaClient, + "doc:doc1", + "admin", + "group#member", + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) + if err != nil { + t.Error(err) + } + + if output != &expectedResponse { + t.Errorf("Expect %v but actual %v", expectedResponse, *output) + } +} + +func TestListUsersWithConsistency(t *testing.T) { + t.Parallel() + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + mockFgaClient := mock_client.NewMockSdkClient(mockCtrl) + + mockExecute := mock_client.NewMockSdkClientListUsersRequestInterface(mockCtrl) + + expectedResponse := client.ClientListUsersResponse{ + Users: []openfga.User{ + { + Object: &openfga.FgaObject{ + Type: "user", + Id: "anne", + }, + }, + }, + } + + mockExecute.EXPECT().Execute().Return(&expectedResponse, nil) + + mockRequest := mock_client.NewMockSdkClientListUsersRequestInterface(mockCtrl) + options := client.ClientListUsersOptions{ + Consistency: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + } + mockRequest.EXPECT().Options(options).Return(mockExecute) + + mockBody := mock_client.NewMockSdkClientListUsersRequestInterface(mockCtrl) + + contextualTuples := []client.ClientContextualTupleKey{} + userFilters := []openfga.UserTypeFilter{ + { + Type: "user", + }, + } + + body := client.ClientListUsersRequest{ + Object: openfga.FgaObject{ + Type: "doc", + Id: "doc1", + }, + Relation: "admin", + UserFilters: userFilters, + ContextualTuples: contextualTuples, + Context: queryContext, + } + mockBody.EXPECT().Body(body).Return(mockRequest) + + mockFgaClient.EXPECT().ListUsers(context.Background()).Return(mockBody) + + output, err := listUsers( + mockFgaClient, + "doc:doc1", + "admin", + "user", + contextualTuples, + queryContext, + openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + ) if err != nil { t.Error(err) } diff --git a/cmd/query/query.go b/cmd/query/query.go index 3d21b45..7df4834 100644 --- a/cmd/query/query.go +++ b/cmd/query/query.go @@ -42,6 +42,11 @@ func init() { QueryCmd.PersistentFlags().String("model-id", "", "Model ID") QueryCmd.PersistentFlags().StringArray("contextual-tuple", []string{}, `Contextual Tuple, output: "user relation object"`) //nolint:lll QueryCmd.PersistentFlags().String("context", "", "Query context (as a JSON string)") + QueryCmd.PersistentFlags().String( + "consistency", + "", + "Consistency preference for the request. Valid options are HIGHER_CONSISTENCY and MINIMIZE_LATENCY.", + ) err := QueryCmd.MarkPersistentFlagRequired("store-id") if err != nil { diff --git a/internal/cmdutils/get-consistency.go b/internal/cmdutils/get-consistency.go new file mode 100644 index 0000000..e230b65 --- /dev/null +++ b/internal/cmdutils/get-consistency.go @@ -0,0 +1,55 @@ +/* +Copyright © 2023 OpenFGA + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmdutils + +import ( + "fmt" + "strings" + + openfga "github.com/openfga/go-sdk" + "github.com/spf13/cobra" +) + +func ParseConsistency(consistency string) (*openfga.ConsistencyPreference, error) { + if consistency == "" { + return openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), nil + } + + val := openfga.ConsistencyPreference(consistency) + if val.IsValid() { + return &val, nil + } + + val = openfga.ConsistencyPreference(strings.ToUpper(consistency)) + if val.IsValid() { + return &val, nil + } + + return nil, fmt.Errorf( //nolint:err113 + "invalid value '%s' for consistency. Valid values are HIGHER_CONSISTENCY and MINIMIZE_LATENCY", + consistency, + ) +} + +func ParseConsistencyFromCmd(cmd *cobra.Command) (*openfga.ConsistencyPreference, error) { + consistency, err := cmd.Flags().GetString("consistency") + if err != nil { + return nil, fmt.Errorf("failed to parse consistency due to %w", err) + } + + return ParseConsistency(consistency) +} diff --git a/internal/cmdutils/get-consistency_test.go b/internal/cmdutils/get-consistency_test.go new file mode 100644 index 0000000..715d061 --- /dev/null +++ b/internal/cmdutils/get-consistency_test.go @@ -0,0 +1,74 @@ +/* +Copyright © 2023 OpenFGA + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmdutils_test + +import ( + "testing" + + openfga "github.com/openfga/go-sdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/openfga/cli/internal/cmdutils" +) + +func TestGetConsistency(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + stringVal string + expected *openfga.ConsistencyPreference + err string + }{ + { + name: "handles parsing correct value", + stringVal: "HIGHER_CONSISTENCY", + expected: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + }, + { + name: "handles parsing value from lowercase", + stringVal: "higher_consistency", + expected: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + }, + { + name: "handles no value", + stringVal: "", + expected: openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + }, + { + name: "throws for unknown values", + stringVal: "invalid", + err: "invalid value 'invalid' for consistency", + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + consistency, err := cmdutils.ParseConsistency(test.stringVal) + + if err == nil { + assert.Equal(t, test.expected, consistency) + } else { + require.Error(t, err) + assert.ErrorContains(t, err, test.err) + } + }) + } +} From fc22d9c810796fe22e0caa7a4a578ff32da30bbe Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 2 Sep 2024 17:09:41 +0100 Subject: [PATCH 2/5] docs: add consistency param to command examples --- cmd/query/check.go | 2 +- cmd/query/expand.go | 4 ++-- cmd/query/list-objects.go | 4 ++-- cmd/query/list-relations.go | 4 ++-- cmd/query/list-users.go | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/query/check.go b/cmd/query/check.go index 78f3a9e..7a873e2 100644 --- a/cmd/query/check.go +++ b/cmd/query/check.go @@ -63,7 +63,7 @@ func check( var checkCmd = &cobra.Command{ Use: "check", Short: "Check", - Example: `fga query check --store-id="01H4P8Z95KTXXEP6Z03T75Q984" user:anne can_view document:roadmap --context '{"ip_address":"127.0.0.1"}'`, //nolint:lll + Example: `fga query check --store-id="01H4P8Z95KTXXEP6Z03T75Q984" user:anne can_view document:roadmap --context '{"ip_address":"127.0.0.1"}' --consistency "HIGHER_CONSISTENCY"`, //nolint:lll Long: "Check if a user has a particular relation with an object.", Args: cobra.ExactArgs(3), //nolint:mnd RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/query/expand.go b/cmd/query/expand.go index feed6ad..55ff1db 100644 --- a/cmd/query/expand.go +++ b/cmd/query/expand.go @@ -58,8 +58,8 @@ var expandCmd = &cobra.Command{ Use: "expand", Short: "Expand", Long: "Expands the relationships in userset tree format.", - Example: `fga query expand --store-id="01H4P8Z95KTXXEP6Z03T75Q984" can_view document:roadmap`, - Args: cobra.ExactArgs(2), //nolint:mnd + Example: `fga query expand --store-id="01H4P8Z95KTXXEP6Z03T75Q984" can_view document:roadmap --consistency "HIGHER_CONSISTENCY"`, //nolint:lll + Args: cobra.ExactArgs(2), //nolint:mnd,lll RunE: func(cmd *cobra.Command, args []string) error { clientConfig := cmdutils.GetClientConfig(cmd) diff --git a/cmd/query/list-objects.go b/cmd/query/list-objects.go index c5194e0..837e184 100644 --- a/cmd/query/list-objects.go +++ b/cmd/query/list-objects.go @@ -64,8 +64,8 @@ var listObjectsCmd = &cobra.Command{ Use: "list-objects", Short: "List Objects", Long: "List the objects of a certain type that a user has a particular relation to.", - Example: `fga query list-objects --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document --contextual-tuple "user:anne can_view folder:product" --contextual-tuple "folder:product parent document:roadmap"`, //nolint:lll - Args: cobra.ExactArgs(3), //nolint:mnd,lll + Example: `fga query list-objects --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document --contextual-tuple "user:anne can_view folder:product" --contextual-tuple "folder:product parent document:roadmap" --consistency "HIGHER_CONSISTENCY"`, //nolint:lll + Args: cobra.ExactArgs(3), //nolint:mnd,lll RunE: func(cmd *cobra.Command, args []string) error { clientConfig := cmdutils.GetClientConfig(cmd) diff --git a/cmd/query/list-relations.go b/cmd/query/list-relations.go index a351cd3..0afebae 100644 --- a/cmd/query/list-relations.go +++ b/cmd/query/list-relations.go @@ -127,8 +127,8 @@ var listRelationsCmd = &cobra.Command{ Use: "list-relations", Short: "List Relations", Long: "List relations that a user has with an object.", - Example: `fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap --relation can_view`, //nolint:lll - Args: cobra.ExactArgs(2), //nolint:mnd,lll + Example: `fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap --relation can_view --consistency "HIGHER_CONSISTENCY"`, //nolint:lll + Args: cobra.ExactArgs(2), //nolint:mnd,lll RunE: func(cmd *cobra.Command, args []string) error { clientConfig := cmdutils.GetClientConfig(cmd) fgaClient, err := clientConfig.GetFgaClient() diff --git a/cmd/query/list-users.go b/cmd/query/list-users.go index 84d35ac..308a8ea 100644 --- a/cmd/query/list-users.go +++ b/cmd/query/list-users.go @@ -94,7 +94,7 @@ var listUsersCmd = &cobra.Command{ Use: "list-users", Short: "List users", Long: "List all users that have a certain relation with a particular object", - Example: `fga query list-users --store-id=01H0H015178Y2V4CX10C2KGHF4 --object document:roadmap --relation can_view`, + Example: `fga query list-users --store-id=01H0H015178Y2V4CX10C2KGHF4 --object document:roadmap --relation can_view --consistency "HIGHER_CONSISTENCY"`, //nolint:lll RunE: func(cmd *cobra.Command, _ []string) error { clientConfig := cmdutils.GetClientConfig(cmd) fgaClient, err := clientConfig.GetFgaClient() From 12ba9f5a435c3ec59822a579ec4a25c33d4ad307 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Mon, 2 Sep 2024 17:27:53 +0100 Subject: [PATCH 3/5] feat(tuple): support consistency in the tuple read command --- cmd/store/export.go | 2 +- cmd/tuple/read.go | 24 +++++++++++++++++++--- cmd/tuple/read_test.go | 45 +++++++++++++++++++++++++++++++++++++----- internal/tuple/read.go | 11 ++++++++++- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/cmd/store/export.go b/cmd/store/export.go index a729ace..d96a30c 100644 --- a/cmd/store/export.go +++ b/cmd/store/export.go @@ -68,7 +68,7 @@ func buildStoreData(config fga.ClientConfig, fgaClient client.SdkClient, maxTupl // get the tuples maxPages := int(math.Ceil(float64(maxTupleCount) / float64(tuple.DefaultReadPageSize))) - rawTuples, err := tuple.Read(fgaClient, &client.ClientReadRequest{}, maxPages) + rawTuples, err := tuple.Read(fgaClient, &client.ClientReadRequest{}, maxPages, nil) if err != nil { return nil, fmt.Errorf("unable to read tuples: %w", err) } diff --git a/cmd/tuple/read.go b/cmd/tuple/read.go index 998e044..dce362c 100644 --- a/cmd/tuple/read.go +++ b/cmd/tuple/read.go @@ -88,7 +88,14 @@ func (r readResponse) toCsvDTO() ([]readResponseCSVDTO, error) { return readResponseDTO, nil } -func read(fgaClient client.SdkClient, user string, relation string, object string, maxPages int) ( +func read( + fgaClient client.SdkClient, + user string, + relation string, + object string, + maxPages int, + consistency *openfga.ConsistencyPreference, +) ( *readResponse, error, ) { body := &client.ClientReadRequest{} @@ -104,7 +111,7 @@ func read(fgaClient client.SdkClient, user string, relation string, object strin body.Object = &object } - response, err := tuple.Read(fgaClient, body, maxPages) + response, err := tuple.Read(fgaClient, body, maxPages, consistency) if err != nil { return nil, err //nolint:wrapcheck } @@ -142,7 +149,12 @@ var readCmd = &cobra.Command{ return fmt.Errorf("failed to parse max pages due to %w", err) } - response, err := read(fgaClient, user, relation, object, maxPages) + consistency, err := cmdutils.ParseConsistencyFromCmd(cmd) + if err != nil { + return fmt.Errorf("error parsing consistency for check: %w", err) + } + + response, err := read(fgaClient, user, relation, object, maxPages, consistency) if err != nil { return err } @@ -180,6 +192,12 @@ func init() { readCmd.Flags().String("output-format", "json", "Specifies the format for data presentation. Valid options: "+ "json, simple-json, csv, and yaml.") readCmd.Flags().Bool("simple-output", false, "Output data in simpler version. (It can be used by write and delete commands)") //nolint:lll + readCmd.Flags().String( + "consistency", + "", + "Consistency preference for the request. Valid options are HIGHER_CONSISTENCY and MINIMIZE_LATENCY.", + ) + _ = readCmd.Flags().MarkDeprecated("simple-output", "the flag \"simple-output\" is deprecated and will be removed"+ " in future releases.\nPlease use the \"--output-format=simple-json\" flag instead.") } diff --git a/cmd/tuple/read_test.go b/cmd/tuple/read_test.go index b6ddc6a..462d7e7 100644 --- a/cmd/tuple/read_test.go +++ b/cmd/tuple/read_test.go @@ -50,7 +50,14 @@ func TestReadError(t *testing.T) { mockFgaClient.EXPECT().Read(context.Background()).Return(mockBody) - _, err := read(mockFgaClient, "user:user1", "reader", "document:doc1", 5) + _, err := read( + mockFgaClient, + "user:user1", + "reader", + "document:doc1", + 5, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err == nil { t.Error("Expect error but there is none") } @@ -91,7 +98,14 @@ func TestReadEmpty(t *testing.T) { mockFgaClient.EXPECT().Read(context.Background()).Return(mockBody) - output, err := read(mockFgaClient, "user:user1", "reader", "document:doc1", 5) + output, err := read( + mockFgaClient, + "user:user1", + "reader", + "document:doc1", + 5, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err != nil { t.Error(err) } @@ -166,7 +180,14 @@ func TestReadSinglePage(t *testing.T) { mockFgaClient.EXPECT().Read(context.Background()).Return(mockBody) - output, err := read(mockFgaClient, "user:user1", "reader", "document:doc1", 5) + output, err := read( + mockFgaClient, + "user:user1", + "reader", + "document:doc1", + 5, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err != nil { t.Error(err) } @@ -281,7 +302,14 @@ func TestReadMultiPages(t *testing.T) { mockFgaClient.EXPECT().Read(context.Background()).Return(mockBody2), ) - output, err := read(mockFgaClient, "user:user1", "reader", "document:doc1", 5) + output, err := read( + mockFgaClient, + "user:user1", + "reader", + "document:doc1", + 5, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err != nil { t.Error(err) } @@ -356,7 +384,14 @@ func TestReadMultiPagesMaxLimit(t *testing.T) { mockFgaClient.EXPECT().Read(context.Background()).Return(mockBody) - output, err := read(mockFgaClient, "user:user1", "reader", "document:doc1", 1) + output, err := read( + mockFgaClient, + "user:user1", + "reader", + "document:doc1", + 1, + openfga.CONSISTENCYPREFERENCE_UNSPECIFIED.Ptr(), + ) if err != nil { t.Error(err) } diff --git a/internal/tuple/read.go b/internal/tuple/read.go index 918f4c3..5a9409c 100644 --- a/internal/tuple/read.go +++ b/internal/tuple/read.go @@ -10,7 +10,12 @@ import ( const DefaultReadPageSize int32 = 50 -func Read(fgaClient client.SdkClient, body *client.ClientReadRequest, maxPages int) ( +func Read( + fgaClient client.SdkClient, + body *client.ClientReadRequest, + maxPages int, + consistency *openfga.ConsistencyPreference, +) ( *openfga.ReadResponse, error, ) { tuples := make([]openfga.Tuple, 0) @@ -20,6 +25,10 @@ func Read(fgaClient client.SdkClient, body *client.ClientReadRequest, maxPages i PageSize: openfga.PtrInt32(DefaultReadPageSize), } + if consistency != nil && *consistency != openfga.CONSISTENCYPREFERENCE_UNSPECIFIED { + options.Consistency = consistency + } + for { options.ContinuationToken = &continuationToken From eb1636657a122d33f667c5418b0e84574b0d4367 Mon Sep 17 00:00:00 2001 From: Ewan Harris Date: Tue, 3 Sep 2024 09:34:25 +0100 Subject: [PATCH 4/5] refactor: always request higher consistency on export --- cmd/store/export.go | 8 +++++++- cmd/store/export_test.go | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/store/export.go b/cmd/store/export.go index d96a30c..1a25b45 100644 --- a/cmd/store/export.go +++ b/cmd/store/export.go @@ -22,6 +22,7 @@ import ( "math" "os" + openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "github.com/spf13/cobra" "gopkg.in/yaml.v3" @@ -68,7 +69,12 @@ func buildStoreData(config fga.ClientConfig, fgaClient client.SdkClient, maxTupl // get the tuples maxPages := int(math.Ceil(float64(maxTupleCount) / float64(tuple.DefaultReadPageSize))) - rawTuples, err := tuple.Read(fgaClient, &client.ClientReadRequest{}, maxPages, nil) + rawTuples, err := tuple.Read( + fgaClient, + &client.ClientReadRequest{}, + maxPages, + openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), + ) if err != nil { return nil, fmt.Errorf("unable to read tuples: %w", err) } diff --git a/cmd/store/export_test.go b/cmd/store/export_test.go index b570880..0128dca 100644 --- a/cmd/store/export_test.go +++ b/cmd/store/export_test.go @@ -136,6 +136,7 @@ func TestExportSuccess(t *testing.T) { readOptions := client.ClientReadOptions{ PageSize: openfga.PtrInt32(tuple.DefaultReadPageSize), ContinuationToken: openfga.PtrString(""), + Consistency: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), } mockReadRequest := mock_client.NewMockSdkClientReadRequestInterface(mockCtrl) From 616303dce2f2862f91e420ff2b562df67b748d46 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Tue, 3 Sep 2024 17:49:46 -0500 Subject: [PATCH 5/5] Revert "refactor: always request higher consistency on export" This reverts commit eb1636657a122d33f667c5418b0e84574b0d4367. --- cmd/store/export.go | 8 +------- cmd/store/export_test.go | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/cmd/store/export.go b/cmd/store/export.go index 1a25b45..d96a30c 100644 --- a/cmd/store/export.go +++ b/cmd/store/export.go @@ -22,7 +22,6 @@ import ( "math" "os" - openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/client" "github.com/spf13/cobra" "gopkg.in/yaml.v3" @@ -69,12 +68,7 @@ func buildStoreData(config fga.ClientConfig, fgaClient client.SdkClient, maxTupl // get the tuples maxPages := int(math.Ceil(float64(maxTupleCount) / float64(tuple.DefaultReadPageSize))) - rawTuples, err := tuple.Read( - fgaClient, - &client.ClientReadRequest{}, - maxPages, - openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), - ) + rawTuples, err := tuple.Read(fgaClient, &client.ClientReadRequest{}, maxPages, nil) if err != nil { return nil, fmt.Errorf("unable to read tuples: %w", err) } diff --git a/cmd/store/export_test.go b/cmd/store/export_test.go index 0128dca..b570880 100644 --- a/cmd/store/export_test.go +++ b/cmd/store/export_test.go @@ -136,7 +136,6 @@ func TestExportSuccess(t *testing.T) { readOptions := client.ClientReadOptions{ PageSize: openfga.PtrInt32(tuple.DefaultReadPageSize), ContinuationToken: openfga.PtrString(""), - Consistency: openfga.CONSISTENCYPREFERENCE_HIGHER_CONSISTENCY.Ptr(), } mockReadRequest := mock_client.NewMockSdkClientReadRequestInterface(mockCtrl)