Skip to content

Commit

Permalink
feat: support usage of consistency parameter (#381)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmyjames authored Sep 6, 2024
2 parents 36091f0 + 5bee68d commit cc70665
Show file tree
Hide file tree
Showing 18 changed files with 716 additions and 49 deletions.
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

0 comments on commit cc70665

Please sign in to comment.