From 4f156c90604cdac41e2be8bfdd7f919cba4e3899 Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Wed, 28 Aug 2024 13:59:02 -0700 Subject: [PATCH 1/6] Add `schema convert` and `schema apply` to FGA module --- go.mod | 2 +- go.sum | 2 + internal/cmd/fga.go | 154 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5ba446f..a1424ea 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 - github.com/workos/workos-go/v4 v4.16.0 + github.com/workos/workos-go/v4 v4.21.0 ) require ( diff --git a/go.sum b/go.sum index 6412ddf..60255a5 100644 --- a/go.sum +++ b/go.sum @@ -115,6 +115,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/workos/workos-go/v4 v4.16.0 h1:7BPvVKHI8qhOY/lH4EbBY2CtQs1CDJxXDbfTnYNma3g= github.com/workos/workos-go/v4 v4.16.0/go.mod h1:CwpXdAWhIE3SxV49qBVeYqWV8ojv0A0L9nM1xnho4/c= +github.com/workos/workos-go/v4 v4.21.0 h1:pEoAJzCsBPU46dL6/PwwwS5BrBV8LWOZQp0mERrRPCc= +github.com/workos/workos-go/v4 v4.21.0/go.mod h1:CwpXdAWhIE3SxV49qBVeYqWV8ojv0A0L9nM1xnho4/c= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= diff --git a/internal/cmd/fga.go b/internal/cmd/fga.go index 5ce9e1a..2483f5c 100644 --- a/internal/cmd/fga.go +++ b/internal/cmd/fga.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" "github.com/workos/workos-cli/internal/printer" "github.com/workos/workos-go/v4/pkg/fga" + "github.com/workos/workos-go/v4/pkg/workos_errors" ) var resourceTypesFile string @@ -65,6 +66,14 @@ func init() { queryCmd.Flags().String("order", "", "order in which a list of results should be returned (asc or desc)") fgaCmd.AddCommand(queryCmd) + // schema + convertSchemaCMD.Flags().StringP("output", "o", "json", "output format (schema or json)") + schemaCmd.AddCommand(convertSchemaCMD) + applySchemaCmd.Flags().BoolP("verbose", "v", false, "print extra details about the request") + applySchemaCmd.Flags().Bool("fail-on-warnings", false, "fail if there are warnings") + schemaCmd.AddCommand(applySchemaCmd) + fgaCmd.AddCommand(schemaCmd) + rootCmd.AddCommand(fgaCmd) } @@ -606,6 +615,151 @@ var queryCmd = &cobra.Command{ }, } +var schemaCmd = &cobra.Command{ + Use: "schema", + Short: "Manage your schema", + Long: "A schema is a set of resource types and relations that define the structure of your application. Use this command to manage your schema.", +} + +var convertSchemaCMD = &cobra.Command{ + Use: "convert ", + Short: "Convert a schema to a JSON representation (or JSON to schema)", + Long: "Convert a schema to a JSON representation (or JSON to schema) that can be used to apply resource types.", + Example: `workos fga schema convert schema.txt -o json`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + output, err := cmd.Flags().GetString("output") + if err != nil { + return errors.Wrap(err, "invalid output flag") + } + + bytes, err := os.ReadFile(args[0]) + if err != nil { + return errors.Errorf("error reading input file: %v", err) + } + + var response fga.ConvertSchemaResponse + switch output { + case "json": + schemaString := string(bytes) + response, err = fga.ConvertSchemaToResourceTypes(context.Background(), fga.ConvertSchemaToResourceTypesOpts{ + Schema: schemaString, + }) + if err != nil { + return convertSchemaError(err) + } + case "schema": + var resourceTypesWithVersion fga.ConvertResourceTypesToSchemaOpts + err = json.Unmarshal(bytes, &resourceTypesWithVersion) + if err != nil { + return errors.Errorf("error unmarshalling resource types: %v", err) + } + response, err = fga.ConvertResourceTypesToSchema(context.Background(), resourceTypesWithVersion) + if err != nil { + return convertSchemaError(err) + } + default: + return errors.Errorf("invalid output format: %s", output) + } + + printer.PrintMsg("Version:") + printer.PrintMsg(fmt.Sprintf("%s\n", response.Version)) + + if response.Warnings != nil { + printer.PrintMsg("Warnings:") + for _, warning := range response.Warnings { + printer.PrintMsg(fmt.Sprintf("%s", warning.Message)) + } + printer.PrintMsg("\n") + } + + if response.Schema != nil { + printer.PrintMsg("Schema:") + printer.PrintMsg(fmt.Sprintf("%s", *response.Schema)) + } + + if response.ResourceTypes != nil { + printer.PrintMsg("Resource Types:") + printer.PrintJson(response.ResourceTypes) + } + + return nil + }, +} + +var applySchemaCmd = &cobra.Command{ + Use: "apply ", + Short: "Apply a schema", + Long: "Apply a schema to create or update resource types.", + Example: `workos fga schema apply schema.txt`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + verbose, err := cmd.Flags().GetBool("verbose") + if err != nil { + return errors.Wrap(err, "invalid verbose flag") + } + failOnWarnings, err := cmd.Flags().GetBool("fail-on-warnings") + if err != nil { + return errors.Wrap(err, "invalid fail-on-warnings flag") + } + + bytes, err := os.ReadFile(args[0]) + if err != nil { + return errors.Errorf("error reading input file: %v", err) + } + // Convert schema to resource types + response, err := fga.ConvertSchemaToResourceTypes(context.Background(), fga.ConvertSchemaToResourceTypesOpts{ + Schema: string(bytes), + }) + if err != nil { + return convertSchemaError(err) + } + + if response.Warnings != nil { + printer.PrintMsg("Warnings:") + for _, warning := range response.Warnings { + printer.PrintMsg(fmt.Sprintf("%s", warning.Message)) + } + printer.PrintMsg("\n") + if failOnWarnings { + return errors.New("error applying schema: warnings found (omit --fail-on-warnings to ignore)") + } + } + + printer.PrintMsg("applying schema...") + + if verbose { + printer.PrintJson(response.ResourceTypes) + } + + ops := make([]fga.UpdateResourceTypeOpts, 0) + for _, rt := range response.ResourceTypes { + ops = append(ops, fga.UpdateResourceTypeOpts{ + Type: rt.Type, + Relations: rt.Relations, + }) + } + + _, err = fga.BatchUpdateResourceTypes(context.Background(), ops) + if err != nil { + return errors.Errorf("error applying schema: %v", err) + } + + printer.PrintMsg("Schema applied") + return nil + }, +} + +func convertSchemaError(err error) error { + var target workos_errors.HTTPError + if errors.As(err, &target) { + if len(target.Errors) > 0 { + return errors.Errorf("error converting schema: %s\n\t%s", target.Message, strings.Join(target.Errors, "\n\t")) + } + } + return errors.Errorf("error converting schema: %v", err) +} + func warrantCheckAsString(w fga.WarrantCheck) (string, error) { s := fmt.Sprintf( "%s:%s %s %s:%s", From 0721e4f6c6dcb1b62c6b10ad5bbb5f329646919d Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Wed, 28 Aug 2024 14:23:51 -0700 Subject: [PATCH 2/6] Change flag names --- internal/cmd/fga.go | 57 ++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/internal/cmd/fga.go b/internal/cmd/fga.go index 2483f5c..b955fcc 100644 --- a/internal/cmd/fga.go +++ b/internal/cmd/fga.go @@ -67,7 +67,8 @@ func init() { fgaCmd.AddCommand(queryCmd) // schema - convertSchemaCMD.Flags().StringP("output", "o", "json", "output format (schema or json)") + convertSchemaCMD.Flags().String("to", "json", "output to (schema or json)") + convertSchemaCMD.Flags().String("output-format", "pretty", "output format (pretty or raw). use raw for machine-readable output or writing to a file") schemaCmd.AddCommand(convertSchemaCMD) applySchemaCmd.Flags().BoolP("verbose", "v", false, "print extra details about the request") applySchemaCmd.Flags().Bool("fail-on-warnings", false, "fail if there are warnings") @@ -628,9 +629,13 @@ var convertSchemaCMD = &cobra.Command{ Example: `workos fga schema convert schema.txt -o json`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - output, err := cmd.Flags().GetString("output") + to, err := cmd.Flags().GetString("to") if err != nil { - return errors.Wrap(err, "invalid output flag") + return errors.Wrap(err, "invalid to flag") + } + outputFormat, err := cmd.Flags().GetString("output-format") + if err != nil { + return errors.Wrap(err, "invalid raw flag") } bytes, err := os.ReadFile(args[0]) @@ -639,7 +644,7 @@ var convertSchemaCMD = &cobra.Command{ } var response fga.ConvertSchemaResponse - switch output { + switch to { case "json": schemaString := string(bytes) response, err = fga.ConvertSchemaToResourceTypes(context.Background(), fga.ConvertSchemaToResourceTypesOpts{ @@ -659,30 +664,40 @@ var convertSchemaCMD = &cobra.Command{ return convertSchemaError(err) } default: - return errors.Errorf("invalid output format: %s", output) + return errors.Errorf("invalid conversion: %s", to) } - printer.PrintMsg("Version:") - printer.PrintMsg(fmt.Sprintf("%s\n", response.Version)) + switch outputFormat { + case "pretty": + printer.PrintMsg("Version:") + printer.PrintMsg(fmt.Sprintf("%s\n", response.Version)) - if response.Warnings != nil { - printer.PrintMsg("Warnings:") - for _, warning := range response.Warnings { - printer.PrintMsg(fmt.Sprintf("%s", warning.Message)) + if response.Warnings != nil { + printer.PrintMsg("Warnings:") + for _, warning := range response.Warnings { + printer.PrintMsg(fmt.Sprintf("%s", warning.Message)) + } + printer.PrintMsg("\n") } - printer.PrintMsg("\n") - } - if response.Schema != nil { - printer.PrintMsg("Schema:") - printer.PrintMsg(fmt.Sprintf("%s", *response.Schema)) - } + if response.Schema != nil { + printer.PrintMsg("Schema:") + printer.PrintMsg(fmt.Sprintf("%s", *response.Schema)) + } - if response.ResourceTypes != nil { - printer.PrintMsg("Resource Types:") - printer.PrintJson(response.ResourceTypes) + if response.ResourceTypes != nil { + printer.PrintMsg("Resource Types:") + printer.PrintJson(response.ResourceTypes) + } + case "raw": + if response.Schema != nil { + printer.PrintMsg(*response.Schema) + } else { + printer.PrintJson(response.ResourceTypes) + } + default: + return errors.Errorf("invalid output format: %s", outputFormat) } - return nil }, } From fb0160e61e0ac88bdab7741be64bfc5827eda2cf Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Wed, 28 Aug 2024 14:25:55 -0700 Subject: [PATCH 3/6] Linting issues --- internal/cmd/fga.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/cmd/fga.go b/internal/cmd/fga.go index b955fcc..2792c63 100644 --- a/internal/cmd/fga.go +++ b/internal/cmd/fga.go @@ -675,14 +675,14 @@ var convertSchemaCMD = &cobra.Command{ if response.Warnings != nil { printer.PrintMsg("Warnings:") for _, warning := range response.Warnings { - printer.PrintMsg(fmt.Sprintf("%s", warning.Message)) + printer.PrintMsg(warning.Message) } printer.PrintMsg("\n") } if response.Schema != nil { printer.PrintMsg("Schema:") - printer.PrintMsg(fmt.Sprintf("%s", *response.Schema)) + printer.PrintMsg(*response.Schema) } if response.ResourceTypes != nil { @@ -733,7 +733,7 @@ var applySchemaCmd = &cobra.Command{ if response.Warnings != nil { printer.PrintMsg("Warnings:") for _, warning := range response.Warnings { - printer.PrintMsg(fmt.Sprintf("%s", warning.Message)) + printer.PrintMsg(warning.Message) } printer.PrintMsg("\n") if failOnWarnings { From 4a382dc2e00bb90e897062224be32640637cdca4 Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Wed, 28 Aug 2024 14:27:29 -0700 Subject: [PATCH 4/6] Fix another linter error with type conversion --- internal/cmd/fga.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/cmd/fga.go b/internal/cmd/fga.go index 2792c63..d9f8b71 100644 --- a/internal/cmd/fga.go +++ b/internal/cmd/fga.go @@ -749,10 +749,7 @@ var applySchemaCmd = &cobra.Command{ ops := make([]fga.UpdateResourceTypeOpts, 0) for _, rt := range response.ResourceTypes { - ops = append(ops, fga.UpdateResourceTypeOpts{ - Type: rt.Type, - Relations: rt.Relations, - }) + ops = append(ops, fga.UpdateResourceTypeOpts(rt)) } _, err = fga.BatchUpdateResourceTypes(context.Background(), ops) From fc534ea4fa64e685978d21bc76ddd312b671f59b Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Wed, 28 Aug 2024 16:26:16 -0700 Subject: [PATCH 5/6] CR feedback --- go.sum | 2 -- internal/cmd/fga.go | 63 ++++++++++++++++++++++----------------------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/go.sum b/go.sum index 60255a5..9f56534 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/workos/workos-go/v4 v4.16.0 h1:7BPvVKHI8qhOY/lH4EbBY2CtQs1CDJxXDbfTnYNma3g= -github.com/workos/workos-go/v4 v4.16.0/go.mod h1:CwpXdAWhIE3SxV49qBVeYqWV8ojv0A0L9nM1xnho4/c= github.com/workos/workos-go/v4 v4.21.0 h1:pEoAJzCsBPU46dL6/PwwwS5BrBV8LWOZQp0mERrRPCc= github.com/workos/workos-go/v4 v4.21.0/go.mod h1:CwpXdAWhIE3SxV49qBVeYqWV8ojv0A0L9nM1xnho4/c= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= diff --git a/internal/cmd/fga.go b/internal/cmd/fga.go index d9f8b71..0de284c 100644 --- a/internal/cmd/fga.go +++ b/internal/cmd/fga.go @@ -68,10 +68,10 @@ func init() { // schema convertSchemaCMD.Flags().String("to", "json", "output to (schema or json)") - convertSchemaCMD.Flags().String("output-format", "pretty", "output format (pretty or raw). use raw for machine-readable output or writing to a file") + convertSchemaCMD.Flags().BoolP("raw", "r", false, "output raw JSON or raw schema to pipe into a file") schemaCmd.AddCommand(convertSchemaCMD) applySchemaCmd.Flags().BoolP("verbose", "v", false, "print extra details about the request") - applySchemaCmd.Flags().Bool("fail-on-warnings", false, "fail if there are warnings") + applySchemaCmd.Flags().Bool("strict", false, "fail if there are warnings") schemaCmd.AddCommand(applySchemaCmd) fgaCmd.AddCommand(schemaCmd) @@ -633,7 +633,7 @@ var convertSchemaCMD = &cobra.Command{ if err != nil { return errors.Wrap(err, "invalid to flag") } - outputFormat, err := cmd.Flags().GetString("output-format") + raw, err := cmd.Flags().GetBool("raw") if err != nil { return errors.Wrap(err, "invalid raw flag") } @@ -667,37 +667,36 @@ var convertSchemaCMD = &cobra.Command{ return errors.Errorf("invalid conversion: %s", to) } - switch outputFormat { - case "pretty": - printer.PrintMsg("Version:") - printer.PrintMsg(fmt.Sprintf("%s\n", response.Version)) - - if response.Warnings != nil { - printer.PrintMsg("Warnings:") - for _, warning := range response.Warnings { - printer.PrintMsg(warning.Message) - } - printer.PrintMsg("\n") - } - - if response.Schema != nil { - printer.PrintMsg("Schema:") - printer.PrintMsg(*response.Schema) - } - - if response.ResourceTypes != nil { - printer.PrintMsg("Resource Types:") - printer.PrintJson(response.ResourceTypes) - } - case "raw": + if raw { if response.Schema != nil { printer.PrintMsg(*response.Schema) } else { printer.PrintJson(response.ResourceTypes) } - default: - return errors.Errorf("invalid output format: %s", outputFormat) + return nil } + + printer.PrintMsg("Version:") + printer.PrintMsg(fmt.Sprintf("%s\n", response.Version)) + + if response.Warnings != nil { + printer.PrintMsg("Warnings:") + for _, warning := range response.Warnings { + printer.PrintMsg(warning.Message) + } + printer.PrintMsg("\n") + } + + if response.Schema != nil { + printer.PrintMsg("Schema:") + printer.PrintMsg(*response.Schema) + } + + if response.ResourceTypes != nil { + printer.PrintMsg("Resource Types:") + printer.PrintJson(response.ResourceTypes) + } + return nil }, } @@ -713,9 +712,9 @@ var applySchemaCmd = &cobra.Command{ if err != nil { return errors.Wrap(err, "invalid verbose flag") } - failOnWarnings, err := cmd.Flags().GetBool("fail-on-warnings") + strict, err := cmd.Flags().GetBool("strict") if err != nil { - return errors.Wrap(err, "invalid fail-on-warnings flag") + return errors.Wrap(err, "invalid strict flag") } bytes, err := os.ReadFile(args[0]) @@ -736,8 +735,8 @@ var applySchemaCmd = &cobra.Command{ printer.PrintMsg(warning.Message) } printer.PrintMsg("\n") - if failOnWarnings { - return errors.New("error applying schema: warnings found (omit --fail-on-warnings to ignore)") + if strict { + return errors.New("error applying schema: warnings found (omit --strict to ignore)") } } From e1266f31dc9b5ebd0fa65a3ab7da380fc8c629a8 Mon Sep 17 00:00:00 2001 From: Aaron Tainter Date: Wed, 28 Aug 2024 16:30:26 -0700 Subject: [PATCH 6/6] Change raw to output --- internal/cmd/fga.go | 55 +++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/internal/cmd/fga.go b/internal/cmd/fga.go index 0de284c..3ec780b 100644 --- a/internal/cmd/fga.go +++ b/internal/cmd/fga.go @@ -68,7 +68,7 @@ func init() { // schema convertSchemaCMD.Flags().String("to", "json", "output to (schema or json)") - convertSchemaCMD.Flags().BoolP("raw", "r", false, "output raw JSON or raw schema to pipe into a file") + convertSchemaCMD.Flags().String("output", "pretty", "output pretty or raw. use raw for machine-readable output or writing to a file") schemaCmd.AddCommand(convertSchemaCMD) applySchemaCmd.Flags().BoolP("verbose", "v", false, "print extra details about the request") applySchemaCmd.Flags().Bool("strict", false, "fail if there are warnings") @@ -633,9 +633,9 @@ var convertSchemaCMD = &cobra.Command{ if err != nil { return errors.Wrap(err, "invalid to flag") } - raw, err := cmd.Flags().GetBool("raw") + output, err := cmd.Flags().GetString("output") if err != nil { - return errors.Wrap(err, "invalid raw flag") + return errors.Wrap(err, "invalid output flag") } bytes, err := os.ReadFile(args[0]) @@ -667,36 +667,37 @@ var convertSchemaCMD = &cobra.Command{ return errors.Errorf("invalid conversion: %s", to) } - if raw { + switch output { + case "pretty": + printer.PrintMsg("Version:") + printer.PrintMsg(fmt.Sprintf("%s\n", response.Version)) + + if response.Warnings != nil { + printer.PrintMsg("Warnings:") + for _, warning := range response.Warnings { + printer.PrintMsg(warning.Message) + } + printer.PrintMsg("\n") + } + if response.Schema != nil { + printer.PrintMsg("Schema:") printer.PrintMsg(*response.Schema) - } else { - printer.PrintJson(response.ResourceTypes) } - return nil - } - - printer.PrintMsg("Version:") - printer.PrintMsg(fmt.Sprintf("%s\n", response.Version)) - if response.Warnings != nil { - printer.PrintMsg("Warnings:") - for _, warning := range response.Warnings { - printer.PrintMsg(warning.Message) + if response.ResourceTypes != nil { + printer.PrintMsg("Resource Types:") + printer.PrintJson(response.ResourceTypes) } - printer.PrintMsg("\n") - } - - if response.Schema != nil { - printer.PrintMsg("Schema:") - printer.PrintMsg(*response.Schema) - } - - if response.ResourceTypes != nil { - printer.PrintMsg("Resource Types:") - printer.PrintJson(response.ResourceTypes) + case "raw": + if response.Schema != nil { + printer.PrintMsg(*response.Schema) + } else { + printer.PrintJson(response.ResourceTypes) + } + default: + return errors.Errorf("invalid output: %s", output) } - return nil }, }