Skip to content

Commit

Permalink
feat: add support for specifying context/condition on query/write (#219)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Jan 9, 2024
2 parents 70c5f5e + cd4d09c commit a385e2e
Show file tree
Hide file tree
Showing 14 changed files with 246 additions and 68 deletions.
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,14 +586,17 @@ fga tuple **write** <user> <relation> <object> --store-id=<store-id>
* `<user>`: User
* `<relation>`: Relation
* `<object>`: Object
* `--condition-name`: Condition name (optional)
* `--condition-context`: Condition context (optional)
* `--store-id`: Specifies the store id
* `--model-id`: Specifies the model id to target (optional)
* `--file`: Specifies the file name, `yaml` and `json` files are supported
* `--max-tuples-per-write`: Max tuples to send in a single write (optional, default=1)
* `--max-parallel-requests`: Max requests to send in parallel (optional, default=4)

###### Example (with arguments)
`fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap`
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap`
- `fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap --condition-name inOffice --condition-context '{"office_ip":"10.0.1.10"}'`

###### Response
```json5
Expand Down Expand Up @@ -841,15 +844,18 @@ In JSON:
##### Check

###### Command
fga query **check** <user> <relation> <object> [--contextual-tuple "<user> <relation> <object>"]* --store-id=<store-id> [--model-id=<model-id>]
fga query **check** <user> <relation> <object> [--condition] [--contextual-tuple "\<user\> \<relation\> \<object\>"]* --store-id=<store-id> [--model-id=<model-id>]

###### Parameters
* `--store-id`: Specifies the store id
* `--model-id`: Specifies the model id to target (optional)
* `--contextual-tuple`: Contextual tuples
* `--contextual-tuple`: Contextual tuples (optional)
* `--context`: Condition context (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=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"}'`


###### Response
```json5
Expand All @@ -867,9 +873,11 @@ fga query **list-objects** <user> <relation> <object_type> [--contextual-tuple "
* `--store-id`: Specifies the store id
* `--model-id`: Specifies the model id to target (optional)
* `--contextual-tuple`: Contextual tuples (optional) (can be multiple)
* `--context`: Condition context (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 --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"}`

###### Response
```json5
Expand All @@ -890,9 +898,12 @@ fga query **list-objects** <user> <object> [--relation <relation>]* [--contextua
* `--store-id`: Specifies the store id
* `--model-id`: Specifies the model id to target (optional)
* `--contextual-tuple`: Contextual tuples (optional) (can be multiple)
* `--context`: Condition context (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"}`

###### Response
```json5
Expand Down
17 changes: 12 additions & 5 deletions cmd/query/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"

cmdutils2 "github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/output"
)

Expand All @@ -33,12 +33,14 @@ func check(
relation string,
object string,
contextualTuples []client.ClientContextualTupleKey,
queryContext *map[string]interface{},
) (*client.ClientCheckResponse, error) {
body := &client.ClientCheckRequest{
User: user,
Relation: relation,
Object: object,
ContextualTuples: contextualTuples,
Context: queryContext,
}
options := &client.ClientCheckOptions{}

Expand All @@ -54,22 +56,27 @@ 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
Long: "Check if a user has a particular relation with an object.",
Example: `fga check --store-id="01H4P8Z95KTXXEP6Z03T75Q984" user:anne can_view document:roadmap`,
Args: cobra.ExactArgs(3), //nolint:gomnd
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := cmdutils2.GetClientConfig(cmd)
clientConfig := cmdutils.GetClientConfig(cmd)
fgaClient, err := clientConfig.GetFgaClient()
if err != nil {
return fmt.Errorf("failed to initialize FGA Client due to %w", err)
}

contextualTuples, err := cmdutils2.ParseContextualTuples(cmd)
contextualTuples, err := cmdutils.ParseContextualTuples(cmd)
if err != nil {
return fmt.Errorf("error parsing contextual tuples for check: %w", err)
}

response, err := check(fgaClient, args[0], args[1], args[2], contextualTuples)
queryContext, err := cmdutils.ParseQueryContext(cmd, "context")
if err != nil {
return fmt.Errorf("error parsing query context for check: %w", err)
}

response, err := check(fgaClient, args[0], args[1], args[2], contextualTuples, queryContext)
if err != nil {
return fmt.Errorf("failed to check due to %w", err)
}
Expand Down
6 changes: 4 additions & 2 deletions cmd/query/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ func TestCheckWithError(t *testing.T) {
Relation: "writer",
Object: "doc:doc1",
ContextualTuples: contextualTuples,
Context: queryContext,
}
mockBody.EXPECT().Body(body).Return(mockRequest)

mockFgaClient.EXPECT().Check(context.Background()).Return(mockBody)

_, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples)
_, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples, queryContext)
if err == nil {
t.Error("Expect error but there is none")
}
Expand Down Expand Up @@ -84,12 +85,13 @@ func TestCheckWithNoError(t *testing.T) {
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)
output, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples, queryContext)
if err != nil {
t.Error(err)
}
Expand Down
15 changes: 11 additions & 4 deletions cmd/query/list-objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"

cmdutils2 "github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/output"
)

Expand All @@ -34,12 +34,14 @@ func listObjects(
relation string,
objectType string,
contextualTuples []client.ClientContextualTupleKey,
queryContext *map[string]interface{},
) (*client.ClientListObjectsResponse, error) {
body := &client.ClientListObjectsRequest{
User: user,
Relation: relation,
Type: objectType,
ContextualTuples: contextualTuples,
Context: queryContext,
}
options := &client.ClientListObjectsOptions{}

Expand All @@ -59,19 +61,24 @@ var listObjectsCmd = &cobra.Command{
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:gomnd,lll
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := cmdutils2.GetClientConfig(cmd)
clientConfig := cmdutils.GetClientConfig(cmd)

fgaClient, err := clientConfig.GetFgaClient()
if err != nil {
return fmt.Errorf("failed to initialize FGA Client due to %w", err)
}

contextualTuples, err := cmdutils2.ParseContextualTuples(cmd)
contextualTuples, err := cmdutils.ParseContextualTuples(cmd)
if err != nil {
return fmt.Errorf("error parsing contextual tuples for listObjects: %w", err)
}

response, err := listObjects(fgaClient, args[0], args[1], args[2], contextualTuples)
queryContext, err := cmdutils.ParseQueryContext(cmd, "context")
if err != nil {
return fmt.Errorf("error parsing query context for check: %w", err)
}

response, err := listObjects(fgaClient, args[0], args[1], args[2], contextualTuples, queryContext)
if err != nil {
return fmt.Errorf("failed to list objects due to %w", err)
}
Expand Down
5 changes: 3 additions & 2 deletions cmd/query/list-objects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ func TestListObjectsWithError(t *testing.T) {
Relation: "writer",
Type: "doc",
ContextualTuples: contextualTuples,
Context: queryContext,
}
mockBody.EXPECT().Body(body).Return(mockRequest)

mockFgaClient.EXPECT().ListObjects(context.Background()).Return(mockBody)

_, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples)
_, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples, queryContext)
if err == nil {
t.Error("Expect error but there is none")
}
Expand Down Expand Up @@ -85,7 +86,7 @@ func TestListObjectsWithNoError(t *testing.T) {

mockFgaClient.EXPECT().ListObjects(context.Background()).Return(mockBody)

output, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples)
output, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples, nil)
if err != nil {
t.Error(err)
}
Expand Down
19 changes: 13 additions & 6 deletions cmd/query/list-relations.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"

cmdutils2 "github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/fga"
"github.com/openfga/cli/internal/output"
)
Expand All @@ -43,14 +43,14 @@ func getRelationsForType(
return nil, fmt.Errorf("failed to list relations due to %w", err)
}

authorizationModel = *response.AuthorizationModel
authorizationModel = response.GetAuthorizationModel()
} else {
response, err := fgaClient.ReadLatestAuthorizationModel(context.Background()).Execute()
if err != nil {
return nil, fmt.Errorf("failed to list relations due to %w", err)
}

authorizationModel = *response.AuthorizationModel
authorizationModel = response.GetAuthorizationModel()
}

typeDefs := authorizationModel.TypeDefinitions
Expand All @@ -77,6 +77,7 @@ func listRelations(clientConfig fga.ClientConfig,
object string,
relations []string,
contextualTuples []client.ClientContextualTupleKey,
queryContext *map[string]interface{},
) (*client.ClientListRelationsResponse, error) {
if len(relations) < 1 {
relationsForType, err := getRelationsForType(clientConfig, fgaClient, object)
Expand All @@ -99,6 +100,7 @@ func listRelations(clientConfig fga.ClientConfig,
Object: object,
Relations: relations,
ContextualTuples: contextualTuples,
Context: queryContext,
}
options := &client.ClientListRelationsOptions{}

Expand All @@ -122,20 +124,25 @@ var listRelationsCmd = &cobra.Command{
Example: `fga query list-relations --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne document:roadmap --relation can_view`, //nolint:lll
Args: cobra.ExactArgs(2), //nolint:gomnd,lll
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := cmdutils2.GetClientConfig(cmd)
clientConfig := cmdutils.GetClientConfig(cmd)
fgaClient, err := clientConfig.GetFgaClient()
if err != nil {
return fmt.Errorf("failed to initialize FGA Client due to %w", err)
}

contextualTuples, err := cmdutils2.ParseContextualTuples(cmd)
contextualTuples, err := cmdutils.ParseContextualTuples(cmd)
if err != nil {
return fmt.Errorf("error parsing contextual tuples for listRelations: %w", err)
}

queryContext, err := cmdutils.ParseQueryContext(cmd, "context")
if err != nil {
return fmt.Errorf("error parsing query context for check: %w", err)
}

relations, _ := cmd.Flags().GetStringArray("relation")

response, err := listRelations(clientConfig, fgaClient, args[0], args[1], relations, contextualTuples)
response, err := listRelations(clientConfig, fgaClient, args[0], args[1], relations, contextualTuples, queryContext)
if err != nil {
return fmt.Errorf("failed to list relations due to %w", err)
}
Expand Down
21 changes: 15 additions & 6 deletions cmd/query/list-relations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/openfga/go-sdk/client"

"github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/fga"
mock_client "github.com/openfga/cli/internal/mocks"
)
Expand All @@ -18,6 +19,8 @@ var errMockGet = errors.New("mock get model error")

var errMockListRelations = errors.New("mock error")

var queryContext, _ = cmdutils.ParseQueryContextInner(`{"x": 1}`)

func TestListRelationsLatestAuthModelError(t *testing.T) {
t.Parallel()

Expand All @@ -38,7 +41,7 @@ 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)
_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples, queryContext)

if err == nil {
t.Error("Expect error but there is none")
Expand Down Expand Up @@ -67,7 +70,7 @@ 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)
_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples, queryContext)

if err == nil {
t.Error("Expect error but there is none")
Expand Down Expand Up @@ -115,6 +118,7 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) {
Relations: []string{"viewer"},
Object: "doc:doc1",
ContextualTuples: contextualTuples,
Context: queryContext,
}
mockBody.EXPECT().Body(body).Return(mockListRelationsRequest)
gomock.InOrder(
Expand All @@ -124,7 +128,7 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) {

var clientConfig fga.ClientConfig

_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples)
_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples, queryContext)
if err == nil {
t.Error("Expect error but there is none")
}
Expand Down Expand Up @@ -160,7 +164,8 @@ func TestListRelationsLatestAuthModelEmpty(t *testing.T) {
Relations: []string{},
}

response, err := listRelations(clientConfig, mockFgaClient, "doc:doc1", "user:foo", relations, contextualTuples)
response, err := listRelations(clientConfig, mockFgaClient, "doc:doc1", "user:foo", relations,
contextualTuples, queryContext)
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -213,6 +218,7 @@ func TestListRelationsLatestAuthModelList(t *testing.T) {
Relations: []string{"viewer"},
Object: "doc:doc1",
ContextualTuples: contextualTuples,
Context: queryContext,
}
mockBody.EXPECT().Body(body).Return(mockListRelationsRequest)
gomock.InOrder(
Expand All @@ -222,7 +228,8 @@ func TestListRelationsLatestAuthModelList(t *testing.T) {

var clientConfig fga.ClientConfig

output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples)
output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations,
contextualTuples, queryContext)
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -264,6 +271,7 @@ func TestListRelationsMultipleRelations(t *testing.T) {
Relations: []string{"viewer", "editor"},
Object: "doc:doc1",
ContextualTuples: contextualTuples,
Context: queryContext,
}
mockBody.EXPECT().Body(body).Return(mockListRelationsRequest)
gomock.InOrder(
Expand All @@ -272,7 +280,8 @@ func TestListRelationsMultipleRelations(t *testing.T) {

var clientConfig fga.ClientConfig

output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations, contextualTuples)
output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", relations,
contextualTuples, queryContext)
if err != nil {
t.Error(err)
}
Expand Down
Loading

0 comments on commit a385e2e

Please sign in to comment.