From 4833cf46a20f15913dbe191fc31826e99c539445 Mon Sep 17 00:00:00 2001 From: Gabriel Bussolo Date: Sun, 24 Sep 2023 15:32:27 +0300 Subject: [PATCH 1/5] feat: write and delete tuples from json file Signed-off-by: Gabriel Bussolo --- cmd/tuple/delete.go | 38 ++++++++++++++++++++++++++++++++++++-- cmd/tuple/import.go | 5 +++-- cmd/tuple/write.go | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/cmd/tuple/delete.go b/cmd/tuple/delete.go index 911fa44..7907b5d 100644 --- a/cmd/tuple/delete.go +++ b/cmd/tuple/delete.go @@ -18,12 +18,13 @@ package tuple import ( "context" + "encoding/json" "fmt" - "github.com/openfga/cli/internal/cmdutils" "github.com/openfga/cli/internal/output" "github.com/openfga/go-sdk/client" "github.com/spf13/cobra" + "os" ) // deleteCmd represents the delete command. @@ -32,13 +33,37 @@ var deleteCmd = &cobra.Command{ Short: "Delete Relationship Tuples", 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 RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 3 && cmd.Flags().Changed("file") == false { + return fmt.Errorf("you need to specify either 3 arguments or a file") + } 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 = json.Unmarshal(data, &tuples) + if err != nil { + return fmt.Errorf("failed to parse input tuples due to %w", err) + } + err = deleteTuples(fgaClient, tuples) + if err != nil { + return err + } + return output.Display(output.EmptyStruct{}) //nolint:wrapcheck + } body := &client.ClientDeleteTuplesBody{ client.ClientTupleKey{ User: args[0], @@ -56,5 +81,14 @@ var deleteCmd = &cobra.Command{ }, } +func deleteTuples(fgaClient *client.OpenFgaClient, tuples []client.ClientTupleKey) error { + _, err := fgaClient.DeleteTuples(context.Background()).Body(tuples).Execute() + if err != nil { + return fmt.Errorf("failed to delete tuples due to %w", err) + } + return nil +} + func init() { + deleteCmd.Flags().String("file", "", "Tuples file") } diff --git a/cmd/tuple/import.go b/cmd/tuple/import.go index ce638a1..2fe5d4b 100644 --- a/cmd/tuple/import.go +++ b/cmd/tuple/import.go @@ -91,8 +91,9 @@ func importTuples( // 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 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 { diff --git a/cmd/tuple/write.go b/cmd/tuple/write.go index 212d2ba..8de3546 100644 --- a/cmd/tuple/write.go +++ b/cmd/tuple/write.go @@ -18,7 +18,9 @@ package tuple import ( "context" + "encoding/json" "fmt" + "os" "github.com/openfga/cli/internal/cmdutils" "github.com/openfga/cli/internal/output" @@ -32,13 +34,38 @@ var writeCmd = &cobra.Command{ Short: "Create Relationship Tuples", Long: "Add relationship tuples to the store.", Example: "fga tuple write --store-id=01H0H015178Y2V4CX10C2KGHF4 user:anne can_view document:roadmap", - Args: cobra.ExactArgs(3), //nolint:gomnd RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 3 && cmd.Flags().Changed("file") == false { + return fmt.Errorf("you need to specify either 3 arguments or a file") + } 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 != "" { + 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 = json.Unmarshal(data, &tuples) + if err != nil { + return fmt.Errorf("failed to parse input tuples due to %w", err) + } + err = writeTuples(fgaClient, tuples) + if err != nil { + return err + } + return output.Display(output.EmptyStruct{}) //nolint:wrapcheck + } body := &client.ClientWriteTuplesBody{ client.ClientTupleKey{ User: args[0], @@ -56,6 +83,15 @@ var writeCmd = &cobra.Command{ }, } +func writeTuples(fgaClient *client.OpenFgaClient, tuples []client.ClientTupleKey) error { + _, err := fgaClient.WriteTuples(context.Background()).Body(tuples).Execute() + if err != nil { + return fmt.Errorf("failed to write tuples due to %w", err) + } + return nil +} + func init() { writeCmd.Flags().String("model-id", "", "Model ID") + writeCmd.Flags().String("file", "", "Tuples file") } From 829b99fa482df97b23ba8b6f5476d307c76ae8d9 Mon Sep 17 00:00:00 2001 From: Gabriel Bussolo Date: Tue, 26 Sep 2023 11:46:19 +0300 Subject: [PATCH 2/5] fix: keep import compatibility Signed-off-by: Gabriel Bussolo --- cmd/tuple/import.go | 49 +++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/cmd/tuple/import.go b/cmd/tuple/import.go index 2fe5d4b..b41683d 100644 --- a/cmd/tuple/import.go +++ b/cmd/tuple/import.go @@ -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) { @@ -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 { @@ -84,16 +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", - Deprecated: "Use the write command with the flag --file instead.", + 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 { @@ -131,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 } From b63f054d53cd7102c7d5ccfb48b6c90f582d95be Mon Sep 17 00:00:00 2001 From: Gabriel Bussolo Date: Tue, 26 Sep 2023 11:47:11 +0300 Subject: [PATCH 3/5] feat: refactor delete/write tuples to accept --file flag Signed-off-by: Gabriel Bussolo --- cmd/tuple/delete.go | 35 +++++++++++++++++++++++------------ cmd/tuple/write.go | 35 ++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/cmd/tuple/delete.go b/cmd/tuple/delete.go index 7907b5d..35f22d5 100644 --- a/cmd/tuple/delete.go +++ b/cmd/tuple/delete.go @@ -18,12 +18,12 @@ package tuple import ( "context" - "encoding/json" "fmt" "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" "os" ) @@ -54,15 +54,31 @@ var deleteCmd = &cobra.Command{ return fmt.Errorf("failed to read file %s due to %w", fileName, err) } - err = json.Unmarshal(data, &tuples) + err = yaml.Unmarshal(data, &tuples) if err != nil { return fmt.Errorf("failed to parse input tuples due to %w", err) } - err = deleteTuples(fgaClient, tuples) + + 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(output.EmptyStruct{}) //nolint:wrapcheck + + return output.Display(*response) //nolint:wrapcheck } body := &client.ClientDeleteTuplesBody{ client.ClientTupleKey{ @@ -81,14 +97,9 @@ var deleteCmd = &cobra.Command{ }, } -func deleteTuples(fgaClient *client.OpenFgaClient, tuples []client.ClientTupleKey) error { - _, err := fgaClient.DeleteTuples(context.Background()).Body(tuples).Execute() - if err != nil { - return fmt.Errorf("failed to delete tuples due to %w", err) - } - return nil -} - 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 } diff --git a/cmd/tuple/write.go b/cmd/tuple/write.go index 8de3546..fc7552f 100644 --- a/cmd/tuple/write.go +++ b/cmd/tuple/write.go @@ -18,8 +18,8 @@ package tuple import ( "context" - "encoding/json" "fmt" + "gopkg.in/yaml.v3" "os" "github.com/openfga/cli/internal/cmdutils" @@ -49,22 +49,37 @@ var writeCmd = &cobra.Command{ return fmt.Errorf("failed to parse file name due to %w", err) } if fileName != "" { - tuples := []client.ClientTupleKey{} + 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 = json.Unmarshal(data, &tuples) + err = yaml.Unmarshal(data, &tuples) if err != nil { return fmt.Errorf("failed to parse input tuples due to %w", err) } - err = writeTuples(fgaClient, tuples) + + writeRequest := client.ClientWriteRequest{ + Writes: &tuples, + Deletes: &[]client.ClientTupleKey{}, + } + response, err := importTuples(fgaClient, writeRequest, maxTuplesPerWrite, maxParallelRequests) if err != nil { return err } - return output.Display(output.EmptyStruct{}) //nolint:wrapcheck + return output.Display(*response) //nolint:wrapcheck } body := &client.ClientWriteTuplesBody{ client.ClientTupleKey{ @@ -83,15 +98,9 @@ var writeCmd = &cobra.Command{ }, } -func writeTuples(fgaClient *client.OpenFgaClient, tuples []client.ClientTupleKey) error { - _, err := fgaClient.WriteTuples(context.Background()).Body(tuples).Execute() - if err != nil { - return fmt.Errorf("failed to write tuples due to %w", err) - } - return nil -} - 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 } From 83a873c8dba378872b903b558b35d7dd9f335271 Mon Sep 17 00:00:00 2001 From: Gabriel Bussolo Date: Wed, 27 Sep 2023 12:13:03 +0300 Subject: [PATCH 4/5] fix: lint Signed-off-by: Gabriel Bussolo --- cmd/tuple/delete.go | 17 +++++++++++++---- cmd/tuple/write.go | 7 +++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cmd/tuple/delete.go b/cmd/tuple/delete.go index 35f22d5..c68ae72 100644 --- a/cmd/tuple/delete.go +++ b/cmd/tuple/delete.go @@ -19,24 +19,23 @@ 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" - "os" ) // 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", RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 3 && cmd.Flags().Changed("file") == false { - return fmt.Errorf("you need to specify either 3 arguments or a file") - } clientConfig := cmdutils.GetClientConfig(cmd) fgaClient, err := clientConfig.GetFgaClient() if err != nil { @@ -103,3 +102,13 @@ func init() { 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 + } +} diff --git a/cmd/tuple/write.go b/cmd/tuple/write.go index fc7552f..3139f68 100644 --- a/cmd/tuple/write.go +++ b/cmd/tuple/write.go @@ -19,13 +19,13 @@ package tuple import ( "context" "fmt" - "gopkg.in/yaml.v3" "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. @@ -33,11 +33,9 @@ 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", RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 3 && cmd.Flags().Changed("file") == false { - return fmt.Errorf("you need to specify either 3 arguments or a file") - } clientConfig := cmdutils.GetClientConfig(cmd) fgaClient, err := clientConfig.GetFgaClient() if err != nil { @@ -79,6 +77,7 @@ var writeCmd = &cobra.Command{ if err != nil { return err } + return output.Display(*response) //nolint:wrapcheck } body := &client.ClientWriteTuplesBody{ From fab00e382cd86d8fcf90a8c6930109b647469e7e Mon Sep 17 00:00:00 2001 From: Gabriel Bussolo Date: Wed, 27 Sep 2023 13:29:54 +0300 Subject: [PATCH 5/5] feat: update README with the new write/delete --file flag Signed-off-by: Gabriel Bussolo --- README.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8304bd1..14dffad 100644 --- a/README.md +++ b/README.md @@ -566,8 +566,11 @@ fga tuple **write** --store-id= * ``: 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 @@ -575,6 +578,32 @@ fga tuple **write** --store-id= {} ``` +###### 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 @@ -585,8 +614,12 @@ fga tuple **delete** --store-id= * ``: Relation * ``: 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 @@ -594,10 +627,37 @@ fga tuple **delete** --store-id= {} ``` +###### 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