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(query): support contextual tuples #27

Merged
merged 6 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all 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: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,15 +376,15 @@ fga tuples **changes** --type <type> --store-id=<store-id>
##### 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> [--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

###### 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"`

###### JSON Response
```json5
Expand All @@ -396,15 +396,15 @@ fga query **check** <user> <relation> <object> [--contextual-tuple <user> <relat
##### List Objects

###### Command
fga query **list-objects** <user> <relation> <object_type> [--contextual-tuple <user> <relation> <object>]* --store-id=<store-id> [--model-id=<model-id>]
fga query **list-objects** <user> <relation> <object_type> [--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 (optional) (can be multiple)

###### 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"`

###### JSON Response
```json5
Expand All @@ -419,7 +419,7 @@ fga query **list-objects** <user> <relation> <object_type> [--contextual-tuple <
##### List Relations

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

###### Parameters
* `--store-id`: Specifies the store id
Expand All @@ -443,12 +443,11 @@ fga query **list-objects** <user> <object> [--relation <relation>]* [--contextua
##### Expand

###### Command
fga query **expand** <relation> <object> [--contextual-tuple <user> <relation> <object>]* --store-id=<store-id> [--model-id=<model-id>]
fga query **expand** <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 (optional) (can be multiple)

###### Example
`fga query expand --store-id=01H0H015178Y2V4CX10C2KGHF4 can_view document:roadmap`
Expand Down
3 changes: 1 addition & 2 deletions cmd/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ func init() {
ModelsCmd.AddCommand(getCmd)

ModelsCmd.PersistentFlags().String("store-id", "", "Store ID")
ModelsCmd.Flags().String("store-id", "", "Store ID")
err := ModelsCmd.MarkFlagRequired("store-id")
err := ModelsCmd.MarkPersistentFlagRequired("store-id")
if err != nil { //nolint:wsl
fmt.Print(err)
os.Exit(1)
Expand Down
26 changes: 19 additions & 7 deletions cmd/query/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ import (
"github.com/spf13/cobra"
)

func check(fgaClient client.SdkClient, user string, relation string, object string) (string, error) {
func check(
fgaClient client.SdkClient,
user string,
relation string,
object string,
contextualTuples []client.ClientTupleKey,
) (string, error) {
body := &client.ClientCheckRequest{
User: user,
Relation: relation,
Object: object,
User: user,
Relation: relation,
Object: object,
ContextualTuples: &contextualTuples,
}
options := &client.ClientCheckOptions{}

Expand All @@ -50,7 +57,7 @@ func check(fgaClient client.SdkClient, user string, relation string, object stri
var checkCmd = &cobra.Command{
Use: "check",
Short: "Check",
Long: "Check if a user has a particular relation with an object.",
Long: "Check if a user has a particular relation with an object. E.g. \"check user:anne can_view document:roadmap\"",
Args: cobra.ExactArgs(3), //nolint:gomnd
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := cmdutils.GetClientConfig(cmd)
Expand All @@ -59,9 +66,14 @@ var checkCmd = &cobra.Command{
return fmt.Errorf("failed to initialize FGA Client due to %w", err)
}

output, err := check(fgaClient, args[0], args[1], args[2])
contextualTuples, err := cmdutils.ParseContextualTuples(cmd)
if err != nil {
return err
return fmt.Errorf("error parsing contextual tuples for check: %w", err)
}

output, err := check(fgaClient, args[0], args[1], args[2], contextualTuples)
if err != nil {
return fmt.Errorf("error calling check: %w", err)
}
fmt.Print(output)

Expand Down
24 changes: 16 additions & 8 deletions cmd/query/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,21 @@ func TestCheckWithError(t *testing.T) {
mockRequest.EXPECT().Options(options).Return(mockExecute)

mockBody := mock_client.NewMockSdkClientCheckRequestInterface(mockCtrl)
contextualTuples := []client.ClientTupleKey{
{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
}

body := client.ClientCheckRequest{
User: "user:foo",
Relation: "writer",
Object: "doc:doc1",
User: "user:foo",
Relation: "writer",
Object: "doc:doc1",
ContextualTuples: &contextualTuples,
}
mockBody.EXPECT().Body(body).Return(mockRequest)

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

_, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1")
_, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples)
if err == nil {
t.Error("Expect error but there is none")
}
Expand Down Expand Up @@ -71,16 +75,20 @@ func TestCheckWithNoError(t *testing.T) {

mockBody := mock_client.NewMockSdkClientCheckRequestInterface(mockCtrl)

contextualTuples := []client.ClientTupleKey{
{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
}
body := client.ClientCheckRequest{
User: "user:foo",
Relation: "writer",
Object: "doc:doc1",
User: "user:foo",
Relation: "writer",
Object: "doc:doc1",
ContextualTuples: &contextualTuples,
}
mockBody.EXPECT().Body(body).Return(mockRequest)

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

output, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1")
output, err := check(mockFgaClient, "user:foo", "writer", "doc:doc1", contextualTuples)
if err != nil {
t.Error(err)
}
Expand Down
24 changes: 18 additions & 6 deletions cmd/query/list-objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ import (
)

// listObjects in the internal function for calling SDK for list objects.
func listObjects(fgaClient client.SdkClient, user string, relation string, types string) (string, error) {
func listObjects(
fgaClient client.SdkClient,
user string,
relation string,
objectType string,
contextualTuples []client.ClientTupleKey,
) (string, error) {
body := &client.ClientListObjectsRequest{
User: user,
Relation: relation,
Type: types,
User: user,
Relation: relation,
Type: objectType,
ContextualTuples: &contextualTuples,
}
options := &client.ClientListObjectsOptions{}

Expand Down Expand Up @@ -61,9 +68,14 @@ var listObjectsCmd = &cobra.Command{
return fmt.Errorf("failed to initialize FGA Client due to %w", err)
}

output, err := listObjects(fgaClient, args[0], args[1], args[2])
contextualTuples, err := cmdutils.ParseContextualTuples(cmd)
if err != nil {
return err
return fmt.Errorf("error parsing contextual tuples for listObjects: %w", err)
}

output, err := listObjects(fgaClient, args[0], args[1], args[2], contextualTuples)
if err != nil {
return fmt.Errorf("error listing objects: %w", err)
}

fmt.Print(output)
Expand Down
24 changes: 16 additions & 8 deletions cmd/query/list-objects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,20 @@ func TestListObjectsWithError(t *testing.T) {

mockBody := mock_client.NewMockSdkClientListObjectsRequestInterface(mockCtrl)

contextualTuples := []client.ClientTupleKey{
{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
}
body := client.ClientListObjectsRequest{
User: "user:foo",
Relation: "writer",
Type: "doc",
User: "user:foo",
Relation: "writer",
Type: "doc",
ContextualTuples: &contextualTuples,
}
mockBody.EXPECT().Body(body).Return(mockRequest)

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

_, err := listObjects(mockFgaClient, "user:foo", "writer", "doc")
_, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples)
if err == nil {
t.Error("Expect error but there is none")
}
Expand All @@ -67,16 +71,20 @@ func TestListObjectsWithNoError(t *testing.T) {

mockBody := mock_client.NewMockSdkClientListObjectsRequestInterface(mockCtrl)

contextualTuples := []client.ClientTupleKey{
{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
}
body := client.ClientListObjectsRequest{
User: "user:foo",
Relation: "writer",
Type: "doc",
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")
output, err := listObjects(mockFgaClient, "user:foo", "writer", "doc", contextualTuples)
if err != nil {
t.Error(err)
}
Expand Down
17 changes: 12 additions & 5 deletions cmd/query/list-relations.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func listRelations(clientConfig fga.ClientConfig,
fgaClient client.SdkClient,
user string,
objects string,
contextualTuples []client.ClientTupleKey,
) (string, error) {
var authorizationModel openfga.AuthorizationModel

Expand Down Expand Up @@ -69,9 +70,10 @@ func listRelations(clientConfig fga.ClientConfig,
}

body := &client.ClientListRelationsRequest{
User: user,
Object: objects,
Relations: relations,
User: user,
Object: objects,
Relations: relations,
ContextualTuples: &contextualTuples,
}
options := &client.ClientListRelationsOptions{}

Expand Down Expand Up @@ -103,9 +105,14 @@ var listRelationsCmd = &cobra.Command{
return fmt.Errorf("failed to initialize FGA Client due to %w", err)
}

output, err := listRelations(clientConfig, fgaClient, args[0], args[1])
contextualTuples, err := cmdutils.ParseContextualTuples(cmd)
if err != nil {
return err
return fmt.Errorf("error parsing contextual tuples for listRelations: %w", err)
}

output, err := listRelations(clientConfig, fgaClient, args[0], args[1], contextualTuples)
if err != nil {
return fmt.Errorf("error listing relations: %w", err)
}

fmt.Print(output)
Expand Down
36 changes: 26 additions & 10 deletions cmd/query/list-relations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ func TestListRelationsLatestAuthModelError(t *testing.T) {

var clientConfig fga.ClientConfig

_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1")
contextualTuples := []client.ClientTupleKey{
{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
}
_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples)

if err == nil {
t.Error("Expect error but there is none")
}
Expand All @@ -56,7 +60,11 @@ func TestListRelationsAuthModelSpecifiedError(t *testing.T) {
AuthorizationModelID: "01GXSA8YR785C4FYS3C0RTG7B1",
}

_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1")
contextualTuples := []client.ClientTupleKey{
{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
}
_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples)

if err == nil {
t.Error("Expect error but there is none")
}
Expand Down Expand Up @@ -94,10 +102,14 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) {

mockBody := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl)

contextualTuples := []client.ClientTupleKey{
{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
}
body := client.ClientListRelationsRequest{
User: "user:foo",
Relations: []string{"viewer"},
Object: "doc:doc1",
User: "user:foo",
Relations: []string{"viewer"},
Object: "doc:doc1",
ContextualTuples: &contextualTuples,
}
mockBody.EXPECT().Body(body).Return(mockListRelationsRequest)
gomock.InOrder(
Expand All @@ -107,7 +119,7 @@ func TestListRelationsLatestAuthModelListError(t *testing.T) {

var clientConfig fga.ClientConfig

_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1")
_, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples)
if err == nil {
t.Error("Expect error but there is none")
}
Expand Down Expand Up @@ -147,10 +159,14 @@ func TestListRelationsLatestAuthModelList(t *testing.T) {

mockBody := mock_client.NewMockSdkClientListRelationsRequestInterface(mockCtrl)

contextualTuples := []client.ClientTupleKey{
{User: "user:foo", Relation: "admin", Object: "doc:doc1"},
}
body := client.ClientListRelationsRequest{
User: "user:foo",
Relations: []string{"viewer"},
Object: "doc:doc1",
User: "user:foo",
Relations: []string{"viewer"},
Object: "doc:doc1",
ContextualTuples: &contextualTuples,
}
mockBody.EXPECT().Body(body).Return(mockListRelationsRequest)
gomock.InOrder(
Expand All @@ -160,7 +176,7 @@ func TestListRelationsLatestAuthModelList(t *testing.T) {

var clientConfig fga.ClientConfig

output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1")
output, err := listRelations(clientConfig, mockFgaClient, "user:foo", "doc:doc1", contextualTuples)
if err != nil {
t.Error(err)
}
Expand Down
8 changes: 5 additions & 3 deletions cmd/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ func init() {
QueryCmd.AddCommand(listRelationsCmd)

QueryCmd.PersistentFlags().String("store-id", "", "Store ID")
QueryCmd.Flags().String("store-id", "", "Store ID")
err := QueryCmd.MarkFlagRequired("store-id")
if err != nil { //nolint:wsl
QueryCmd.PersistentFlags().String("model-id", "", "Model ID")
QueryCmd.PersistentFlags().StringArray("contextual-tuple", []string{}, `Contextual Tuple, format: "user relation object"`) //nolint:lll

err := QueryCmd.MarkPersistentFlagRequired("store-id")
if err != nil {
fmt.Print(err)
os.Exit(1)
}
Expand Down
Loading
Loading