Skip to content

Commit

Permalink
feat(query): support contextual tuples (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhamzeh authored Jul 5, 2023
2 parents 68380b1 + db2ede1 commit 41a3043
Show file tree
Hide file tree
Showing 15 changed files with 281 additions and 67 deletions.
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

0 comments on commit 41a3043

Please sign in to comment.