Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support usage of consistency parameter #381

Merged
merged 6 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -981,10 +981,11 @@ fga query **check** <user> <relation> <object> [--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
Expand All @@ -1004,10 +1005,11 @@ fga query **list-objects** <user> <relation> <object_type> [--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
Expand All @@ -1029,11 +1031,12 @@ fga query **list-relations** <user> <object> [--relation <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
Expand All @@ -1052,6 +1055,7 @@ fga query **expand** <relation> <object> --store-id=<store-id> [--model-id=<mode
###### Parameters
* `--store-id`: Specifies the store id
* `--model-id`: Specifies the model id to target (optional)
* `--consistency`: Consistency preference (optional)
###### Example
`fga query expand --store-id=01H0H015178Y2V4CX10C2KGHF4 can_view document:roadmap`
Expand Down Expand Up @@ -1090,11 +1094,12 @@ fga query **list-users** --object <object> --relation <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
Expand Down
16 changes: 14 additions & 2 deletions cmd/query/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"

openfga "github.com/openfga/go-sdk"
"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"

Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -56,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 {
Expand All @@ -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)
}
Expand Down
78 changes: 76 additions & 2 deletions cmd/query/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
}
Expand Down
27 changes: 22 additions & 5 deletions cmd/query/expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,32 @@ import (
"context"
"fmt"

openfga "github.com/openfga/go-sdk"
"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"

"github.com/openfga/cli/internal/cmdutils"
"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)
}
Expand All @@ -46,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)

Expand All @@ -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
}
Expand Down
61 changes: 57 additions & 4 deletions cmd/query/expand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"reflect"
"testing"

openfga "github.com/openfga/go-sdk"
"github.com/openfga/go-sdk/client"
"go.uber.org/mock/gomock"

Expand All @@ -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")
}
Expand All @@ -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)
}
Expand Down
Loading
Loading