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",