From 0d772185e2739b500eef0ba1fb1a6689aead51ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Thu, 5 Jun 2025 18:58:39 -0300 Subject: [PATCH 1/4] refactor: print failed tuples in input format --- CHANGELOG.md | 3 + README.md | 10 ++-- cmd/tuple/failed_output.go | 119 +++++++++++++++++++++++++++++++++++++ cmd/tuple/write.go | 17 ++++++ 4 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 cmd/tuple/failed_output.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b1702e01..5c01329c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ #### [Unreleased](https://github.com/openfga/cli/compare/v0.6.6...HEAD) +Added: +- Failed tuples are now written to `stderr` in the format of the input file (#449) + #### [0.6.6](https://github.com/openfga/cli/compare/v0.6.5...v0.6.6) (2025-04-23) diff --git a/README.md b/README.md index 39565a74..971884c3 100644 --- a/README.md +++ b/README.md @@ -785,15 +785,13 @@ If using a `json` file, the format should be: } ``` -In some cases you could want to retry failed tuples (e.g. network connectivity error). To achieve that, you can direct the output to a file: +In some cases you may want to retry failed tuples (for example, due to network issues). Failed tuples are now written to `stderr` in the same format as the input file and can be redirected to another file: -`fga tuple write --file tuples.json' --hide-imported-tuples > results.json` +`fga tuple write --file tuples.json --hide-imported-tuples 2> failed_tuples.json > results.json` -Then, process the file with `jq` to convert it to format that you can send the CLI again: +You can then retry them with: -`jq -c '[.failed[] | {user: .tuple_key.user, relation: .tuple_key.relation, object: .tuple_key.object}]' result.json > failed_tuples.json` - -`fga tuple write --file failed_tuples.json' --hide-imported-tuples ` +`fga tuple write --file failed_tuples.json --hide-imported-tuples` ##### Delete Relationship Tuples diff --git a/cmd/tuple/failed_output.go b/cmd/tuple/failed_output.go new file mode 100644 index 00000000..e00ba462 --- /dev/null +++ b/cmd/tuple/failed_output.go @@ -0,0 +1,119 @@ +package tuple + +import ( + "encoding/json" + "fmt" + "path" + "strings" + + "github.com/gocarina/gocsv" + "github.com/openfga/go-sdk/client" + "gopkg.in/yaml.v3" +) + +const ( + csvFormat = "csv" + yamlFormat = "yaml" + jsonFormat = "json" +) + +// tupleCSVDTO represents a tuple in CSV format. +type tupleCSVDTO struct { + UserType string `csv:"user_type"` + UserID string `csv:"user_id"` + UserRelation string `csv:"user_relation,omitempty"` + Relation string `csv:"relation"` + ObjectType string `csv:"object_type"` + ObjectID string `csv:"object_id"` + ConditionName string `csv:"condition_name,omitempty"` + ConditionContext string `csv:"condition_context,omitempty"` +} + +func tuplesToCSVDTO(tuples []client.ClientTupleKey) ([]tupleCSVDTO, error) { + result := make([]tupleCSVDTO, 0, len(tuples)) + + for _, tupleKey := range tuples { + userParts := strings.SplitN(tupleKey.User, ":", 2) + if len(userParts) != 2 { + continue + } + + userType := userParts[0] + userIDRel := userParts[1] + userID := userIDRel + userRel := "" + + if strings.Contains(userIDRel, "#") { + parts := strings.SplitN(userIDRel, "#", 2) + userID = parts[0] + userRel = parts[1] + } + + objParts := strings.SplitN(tupleKey.Object, ":", 2) + if len(objParts) != 2 { + continue + } + + condName := "" + condCtx := "" + + if tupleKey.Condition != nil { + condName = tupleKey.Condition.Name + + if tupleKey.Condition.Context != nil { + b, err := json.Marshal(tupleKey.Condition.Context) + if err != nil { + return nil, fmt.Errorf("failed to convert condition context to CSV: %w", err) + } + + condCtx = string(b) + } + } + + result = append(result, tupleCSVDTO{ + UserType: userType, + UserID: userID, + UserRelation: userRel, + Relation: tupleKey.Relation, + ObjectType: objParts[0], + ObjectID: objParts[1], + ConditionName: condName, + ConditionContext: condCtx, + }) + } + + return result, nil +} + +func formatFromExtension(fileName string) string { + switch strings.ToLower(path.Ext(fileName)) { + case ".csv": + return csvFormat + case ".yaml", ".yml": + return yamlFormat + default: + return jsonFormat + } +} + +func formatTuples(tuples []client.ClientTupleKey, format string) (string, error) { + switch format { + case csvFormat: + dto, err := tuplesToCSVDTO(tuples) + if err != nil { + return "", err + } + + b, err := gocsv.MarshalBytes(dto) + + return string(b), err + case yamlFormat: + b, err := yaml.Marshal(tuples) + + return string(b), err + default: + b, err := json.MarshalIndent(tuples, "", " ") + + return string(b), err + } +} diff --git a/cmd/tuple/write.go b/cmd/tuple/write.go index 2c4b8af5..e4c6b08b 100644 --- a/cmd/tuple/write.go +++ b/cmd/tuple/write.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "os" "time" "github.com/openfga/go-sdk/client" @@ -191,6 +192,22 @@ func writeTuplesFromFile(ctx context.Context, flags *flag.FlagSet, fgaClient *cl outputResponse["failed"] = response.Failed } + if len(response.Failed) > 0 { + failedFormat := formatFromExtension(fileName) + + failedTuples := make([]client.ClientTupleKey, 0, len(response.Failed)) + for _, f := range response.Failed { + failedTuples = append(failedTuples, f.TupleKey) + } + + out, errFmt2 := formatTuples(failedTuples, failedFormat) + if errFmt2 != nil { + return fmt.Errorf("failed to marshal failed tuples: %w", errFmt2) + } + + fmt.Fprint(os.Stderr, out) + } + outputResponse["total_count"] = len(tuples) outputResponse["successful_count"] = len(response.Successful) outputResponse["failed_count"] = len(response.Failed) From 968579a0594f49664b419af529271a7cd875467f Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Mon, 9 Jun 2025 18:17:52 -0300 Subject: [PATCH 2/4] chore: added tuples in other formats to help on testing --- a.fga.yml | 36 ++++++++++++++++++++++++++++++++ tests/fixtures/basic-tuples.csv | 2 ++ tests/fixtures/basic-tuples.yaml | 3 +++ 3 files changed, 41 insertions(+) create mode 100644 a.fga.yml create mode 100644 tests/fixtures/basic-tuples.csv create mode 100644 tests/fixtures/basic-tuples.yaml diff --git a/a.fga.yml b/a.fga.yml new file mode 100644 index 00000000..13277d7a --- /dev/null +++ b/a.fga.yml @@ -0,0 +1,36 @@ +--- +name: Store Name # store name, optional +# model_file: ./model.fga # a global model that would apply to all tests, optional +# model can be used instead of model_file, optional +model: | + model + schema 1.1 + type user + type folder + relations + define owner: [user] + define parent: [folder] + define can_view: owner or can_view from parent + define can_write: owner or can_write from parentdi + define can_share: owner + +# tuple_file: ./tuples.yaml # global tuples that would apply to all tests, optional +tuples: # global tuples that would apply to all tests, optional + - user: folder:1 + relation: parent + object: folder:2 +tests: # required + - name: test-1 + description: testing that the model works # optional + tuples: + - user: user:anne + relation: owner + object: folder:1 + check: # a set of checks to run + # checks can group multiple users that share the same expected results + - object: folder:2 + users: + - user:beth + - user:carl + assertions: + can_view: false \ No newline at end of file diff --git a/tests/fixtures/basic-tuples.csv b/tests/fixtures/basic-tuples.csv new file mode 100644 index 00000000..673da1ce --- /dev/null +++ b/tests/fixtures/basic-tuples.csv @@ -0,0 +1,2 @@ +user_type,user_id,user_relation,relation,object_type,object_id +user,anne,,owner,group,foo diff --git a/tests/fixtures/basic-tuples.yaml b/tests/fixtures/basic-tuples.yaml new file mode 100644 index 00000000..7c4aa70c --- /dev/null +++ b/tests/fixtures/basic-tuples.yaml @@ -0,0 +1,3 @@ +- "user": "user:anne" + "relation": "owner" + "object": "group:foo" \ No newline at end of file From 9dfe2458921b4c768e41e091177577346259f6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Aguiar?= Date: Mon, 9 Jun 2025 18:41:37 -0300 Subject: [PATCH 3/4] Delete a.fga.yml --- a.fga.yml | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 a.fga.yml diff --git a/a.fga.yml b/a.fga.yml deleted file mode 100644 index 13277d7a..00000000 --- a/a.fga.yml +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Store Name # store name, optional -# model_file: ./model.fga # a global model that would apply to all tests, optional -# model can be used instead of model_file, optional -model: | - model - schema 1.1 - type user - type folder - relations - define owner: [user] - define parent: [folder] - define can_view: owner or can_view from parent - define can_write: owner or can_write from parentdi - define can_share: owner - -# tuple_file: ./tuples.yaml # global tuples that would apply to all tests, optional -tuples: # global tuples that would apply to all tests, optional - - user: folder:1 - relation: parent - object: folder:2 -tests: # required - - name: test-1 - description: testing that the model works # optional - tuples: - - user: user:anne - relation: owner - object: folder:1 - check: # a set of checks to run - # checks can group multiple users that share the same expected results - - object: folder:2 - users: - - user:beth - - user:carl - assertions: - can_view: false \ No newline at end of file From 86a01cc93db6d6f3e948824b47657159a7a15cfd Mon Sep 17 00:00:00 2001 From: Andres Aguiar Date: Tue, 24 Jun 2025 10:31:54 -0500 Subject: [PATCH 4/4] chore: move changelog entry to the proper place --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2b71fde..9a884897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ #### [Unreleased](https://github.com/openfga/cli/compare/v0.7.0...HEAD) +Added: +- Failed tuples are now written to `stderr` in the format of the input file (#449) + #### [0.7.0](https://github.com/openfga/cli/compare/v0.6.6...v0.7.0) (2025-06-10) > [!NOTE] @@ -15,9 +18,6 @@ Added: - Include current working directory in the config file resolution (#504) - thanks @OsmanMElsayed -Added: -- Failed tuples are now written to `stderr` in the format of the input file (#449) - Fixed: - Bump OpenFGA to v1.8.13 to resolve a security vulnerability [GHSA-c72g-53hw-82q7](https://github.com/openfga/openfga/security/advisories/GHSA-c72g-53hw-82q7)