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: tuple write/delete cmd accepts file; deprecate tuple import #165

Merged
merged 5 commits into from
Sep 27, 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
66 changes: 63 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,15 +566,44 @@ fga tuple **write** <user> <relation> <object> --store-id=<store-id>
* `<object>`: Object
* `--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
###### Example (with arguments)
`fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap`

###### Response
```json5
{}
```

###### Example (with file)
`fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.json`

###### Response
```json5
{
"successful": [
{
"object":"document:roadmap",
"relation":"writer",
"user":"user:annie"
}
],
"failed": [
{
"tuple_key": {
"object":"document:roadmap",
"relation":"writer",
"user":"carl"
},
"reason":"Write validation error ..."
}
]
}
```

##### Delete Relationship Tuples

###### Command
Expand All @@ -585,19 +614,50 @@ fga tuple **delete** <user> <relation> <object> --store-id=<store-id>
* `<relation>`: Relation
* `<object>`: Object
* `--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
###### Example (with arguments)
`fga tuple delete --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap`

###### Response
```json5
{}
```

###### Example (with file)
`fga tuple delete --store-id=01H0H015178Y2V4CX10C2KGHF4 --file tuples.json`

###### Response
```json5
{
"successful": [
{
"object":"document:roadmap",
"relation":"writer",
"user":"user:annie"
}
],
"failed": [
{
"tuple_key": {
"object":"document:roadmap",
"relation":"writer",
"user":"carl"
},
"reason":"Write validation error ..."
}
]
}
```

If you want to delete all the tuples in a store, you can use the following code:

```
fga tuple read | jq -r '.tuples[] | "fga tuple delete \(.key.user) \(.key.relation) \(.key.object)"' | xargs -0 bash -c
fga tuple read | jq '[.tuples[] | { user: .key.user, relation: .key.relation, object: .key.object }]' > tuples.json
fga tuple delete --file tuples.json
```

##### Read Relationship Tuples
Expand Down
56 changes: 55 additions & 1 deletion cmd/tuple/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,66 @@ package tuple
import (
"context"
"fmt"
"os"

"github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/output"
"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

// deleteCmd represents the delete command.
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete Relationship Tuples",
Args: ExactArgsOrFlag(3, "file"), //nolint:gomnd
Long: "Delete relationship tuples from the store.",
Example: "fga tuple delete --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap",
Args: cobra.ExactArgs(3), //nolint:gomnd
rhamzeh marked this conversation as resolved.
Show resolved Hide resolved
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := cmdutils.GetClientConfig(cmd)
fgaClient, err := clientConfig.GetFgaClient()
if err != nil {
return fmt.Errorf("failed to initialize FGA Client due to %w", err)
}
fileName, err := cmd.Flags().GetString("file")
if err != nil {
return fmt.Errorf("failed to parse file name due to %w", err)
}
if fileName != "" {
var tuples []client.ClientTupleKey

data, err := os.ReadFile(fileName)
if err != nil {
return fmt.Errorf("failed to read file %s due to %w", fileName, err)
}

err = yaml.Unmarshal(data, &tuples)
if err != nil {
return fmt.Errorf("failed to parse input tuples due to %w", err)
}

maxTuplesPerWrite, err := cmd.Flags().GetInt("max-tuples-per-write")
if err != nil {
return fmt.Errorf("failed to parse max tuples per write due to %w", err)
}

maxParallelRequests, err := cmd.Flags().GetInt("max-parallel-requests")
if err != nil {
return fmt.Errorf("failed to parse parallel requests due to %w", err)
}

deleteRequest := client.ClientWriteRequest{
Deletes: &tuples,
Writes: &[]client.ClientTupleKey{},
}
response, err := importTuples(fgaClient, deleteRequest, maxTuplesPerWrite, maxParallelRequests)
if err != nil {
return err
}

return output.Display(*response) //nolint:wrapcheck
}
body := &client.ClientDeleteTuplesBody{
client.ClientTupleKey{
User: args[0],
Expand All @@ -57,4 +97,18 @@ var deleteCmd = &cobra.Command{
}

func init() {
deleteCmd.Flags().String("file", "", "Tuples file")
deleteCmd.Flags().String("model-id", "", "Model ID")
deleteCmd.Flags().Int("max-tuples-per-write", MaxTuplesPerWrite, "Max tuples per write chunk.")
deleteCmd.Flags().Int("max-parallel-requests", MaxParallelRequests, "Max number of requests to issue to the server in parallel.") //nolint:lll
}

func ExactArgsOrFlag(n int, flag string) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) != n && !cmd.Flags().Changed(flag) {
return fmt.Errorf("at least %d arg(s) are required OR the flag --%s", n, flag) //nolint:goerr113
}

return nil
}
}
52 changes: 34 additions & 18 deletions cmd/tuple/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@ type importResponse struct {
Failed []failedWriteResponse `json:"failed"`
}

// importTuples receives a client.ClientWriteRequest and imports the tuples to the store. It can be used to import
// either writes or deletes.
// It returns a pointer to an importResponse and an error.
// The importResponse contains the tuples that were successfully imported and the tuples that failed to be imported.
// Deletes and writes are put together in the same importResponse.
func importTuples(
fgaClient client.SdkClient,
tuples []client.ClientTupleKey,
body client.ClientWriteRequest,
maxTuplesPerWrite int,
maxParallelRequests int,
) (*importResponse, error) {
Expand All @@ -58,22 +63,29 @@ func importTuples(
},
}

deletes := []client.ClientTupleKey{}
body := &client.ClientWriteRequest{
Writes: &tuples,
Deletes: &deletes,
}

response, err := fgaClient.Write(context.Background()).Body(*body).Options(options).Execute()
response, err := fgaClient.Write(context.Background()).Body(body).Options(options).Execute()
if err != nil {
return nil, fmt.Errorf("failed to import tuples due to %w", err)
}

successfulWrites := []client.ClientTupleKey{}
failedWrites := []failedWriteResponse{}
successfulWrites, failedWrites := processWrites(response.Writes)
successfulDeletes, failedDeletes := processWrites(response.Deletes)

result := importResponse{
Successful: append(successfulWrites, successfulDeletes...),
Failed: append(failedWrites, failedDeletes...),
}

return &result, nil
}

func processWrites(writes []client.ClientWriteSingleResponse) ([]client.ClientTupleKey, []failedWriteResponse) {
var (
successfulWrites []client.ClientTupleKey
failedWrites []failedWriteResponse
)

for index := 0; index < len(response.Writes); index++ {
write := response.Writes[index]
for _, write := range writes {
if write.Status == client.SUCCESS {
successfulWrites = append(successfulWrites, write.TupleKey)
} else {
Expand All @@ -84,15 +96,14 @@ func importTuples(
}
}

result := importResponse{Successful: successfulWrites, Failed: failedWrites}

return &result, nil
return successfulWrites, failedWrites
}

// importCmd represents the import command.
var importCmd = &cobra.Command{
Use: "import",
Short: "Import Relationship Tuples",
Use: "import",
Short: "Import Relationship Tuples",
Deprecated: "use the write/delete command with the flag --file instead",
Long: "Imports Relationship Tuples to the store. " +
"This will write the tuples in chunks and at the end will report the tuple chunks that failed.",
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -130,7 +141,12 @@ var importCmd = &cobra.Command{
return fmt.Errorf("failed to parse input tuples due to %w", err)
}

result, err := importTuples(fgaClient, tuples, maxTuplesPerWrite, maxParallelRequests)
writeRequest := client.ClientWriteRequest{
Writes: &tuples,
Deletes: &[]client.ClientTupleKey{},
}

result, err := importTuples(fgaClient, writeRequest, maxTuplesPerWrite, maxParallelRequests)
if err != nil {
return err
}
Expand Down
46 changes: 45 additions & 1 deletion cmd/tuple/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,67 @@ package tuple
import (
"context"
"fmt"
"os"

"github.com/openfga/cli/internal/cmdutils"
"github.com/openfga/cli/internal/output"
"github.com/openfga/go-sdk/client"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

// writeCmd represents the write command.
var writeCmd = &cobra.Command{
Use: "write",
Short: "Create Relationship Tuples",
Long: "Add relationship tuples to the store.",
Args: ExactArgsOrFlag(3, "file"), //nolint:gomnd
Example: "fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap",
Args: cobra.ExactArgs(3), //nolint:gomnd
rhamzeh marked this conversation as resolved.
Show resolved Hide resolved
RunE: func(cmd *cobra.Command, args []string) error {
clientConfig := cmdutils.GetClientConfig(cmd)
fgaClient, err := clientConfig.GetFgaClient()
if err != nil {
return fmt.Errorf("failed to initialize FGA Client due to %w", err)
}

fileName, err := cmd.Flags().GetString("file")
if err != nil {
return fmt.Errorf("failed to parse file name due to %w", err)
}
if fileName != "" {
maxTuplesPerWrite, err := cmd.Flags().GetInt("max-tuples-per-write")
if err != nil {
return fmt.Errorf("failed to parse max tuples per write due to %w", err)
}

maxParallelRequests, err := cmd.Flags().GetInt("max-parallel-requests")
if err != nil {
return fmt.Errorf("failed to parse parallel requests due to %w", err)
}

var tuples []client.ClientTupleKey

data, err := os.ReadFile(fileName)
if err != nil {
return fmt.Errorf("failed to read file %s due to %w", fileName, err)
}

err = yaml.Unmarshal(data, &tuples)
if err != nil {
return fmt.Errorf("failed to parse input tuples due to %w", err)
}

writeRequest := client.ClientWriteRequest{
Writes: &tuples,
Deletes: &[]client.ClientTupleKey{},
}
response, err := importTuples(fgaClient, writeRequest, maxTuplesPerWrite, maxParallelRequests)
if err != nil {
return err
}

return output.Display(*response) //nolint:wrapcheck
}
body := &client.ClientWriteTuplesBody{
client.ClientTupleKey{
User: args[0],
Expand All @@ -58,4 +99,7 @@ var writeCmd = &cobra.Command{

func init() {
writeCmd.Flags().String("model-id", "", "Model ID")
writeCmd.Flags().String("file", "", "Tuples file")
writeCmd.Flags().Int("max-tuples-per-write", MaxTuplesPerWrite, "Max tuples per write chunk.")
writeCmd.Flags().Int("max-parallel-requests", MaxParallelRequests, "Max number of requests to issue to the server in parallel.") //nolint:lll
}