From ddcdd6f69bc31699e533238f50b2c37db22ee3d2 Mon Sep 17 00:00:00 2001 From: ivanauth Date: Sat, 18 Oct 2025 00:30:31 -0400 Subject: [PATCH] fix: convert global command variables to local instances Fixes #556 where tests running with -count=10 would panic with "flag redefined" errors. Package-level cobra.Command globals were reused across test runs, causing flag registration conflicts. Converted all register*Cmd functions to create fresh command instances locally instead of relying on global variables. This ensures InitialiseRootCmd can be called multiple times without conflicts. Added regression test TestMultipleInitialiseRootCmd to verify the fix. --- internal/cmd/backup.go | 18 +++-- internal/cmd/cmd.go | 4 +- internal/cmd/cmd_test.go | 26 ++++++- internal/cmd/context.go | 82 +++++++++++----------- internal/cmd/import.go | 28 ++++---- internal/cmd/mcp.go | 33 +++++---- internal/cmd/preview.go | 26 ++++--- internal/cmd/schema.go | 104 ++++++++++++++-------------- internal/cmd/validate.go | 56 +++++++-------- internal/commands/permission.go | 110 +++++++++++++++--------------- internal/commands/relationship.go | 106 ++++++++++++++-------------- internal/commands/schema.go | 21 +++--- internal/commands/watch.go | 31 ++++----- 13 files changed, 328 insertions(+), 317 deletions(-) diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go index 7b2ca3bf..ad3407e6 100644 --- a/internal/cmd/backup.go +++ b/internal/cmd/backup.go @@ -49,8 +49,8 @@ func withErrorHandling(f cobraRunEFunc) cobraRunEFunc { } } -var ( - backupCmd = &cobra.Command{ +func registerBackupCmd(rootCmd *cobra.Command) { + backupCmd := &cobra.Command{ Use: "backup ", Short: "Create, restore, and inspect permissions system backups", Args: commands.ValidationWrapper(cobra.MaximumNArgs(1)), @@ -58,21 +58,21 @@ var ( RunE: withErrorHandling(backupCreateCmdFunc), } - backupCreateCmd = &cobra.Command{ + backupCreateCmd := &cobra.Command{ Use: "create ", Short: "Backup a permission system to a file", Args: commands.ValidationWrapper(cobra.MaximumNArgs(1)), RunE: withErrorHandling(backupCreateCmdFunc), } - backupRestoreCmd = &cobra.Command{ + backupRestoreCmd := &cobra.Command{ Use: "restore ", Short: "Restore a permission system from a file", Args: commands.ValidationWrapper(commands.StdinOrExactArgs(1)), RunE: backupRestoreCmdFunc, } - backupParseSchemaCmd = &cobra.Command{ + backupParseSchemaCmd := &cobra.Command{ Use: "parse-schema ", Short: "Extract the schema from a backup file", Args: commands.ValidationWrapper(cobra.ExactArgs(1)), @@ -81,7 +81,7 @@ var ( }, } - backupParseRevisionCmd = &cobra.Command{ + backupParseRevisionCmd := &cobra.Command{ Use: "parse-revision ", Short: "Extract the revision from a backup file", Args: commands.ValidationWrapper(cobra.ExactArgs(1)), @@ -90,7 +90,7 @@ var ( }, } - backupParseRelsCmd = &cobra.Command{ + backupParseRelsCmd := &cobra.Command{ Use: "parse-relationships ", Short: "Extract the relationships from a backup file", Args: commands.ValidationWrapper(cobra.ExactArgs(1)), @@ -99,7 +99,7 @@ var ( }, } - backupRedactCmd = &cobra.Command{ + backupRedactCmd := &cobra.Command{ Use: "redact ", Short: "Redact a backup file to remove sensitive information", Args: commands.ValidationWrapper(cobra.ExactArgs(1)), @@ -107,9 +107,7 @@ var ( return backupRedactCmdFunc(cmd, args) }, } -) -func registerBackupCmd(rootCmd *cobra.Command) { rootCmd.AddCommand(backupCmd) registerBackupCreateFlags(backupCmd) diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index e9410bf0..795e6362 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -116,7 +116,6 @@ zed permission check --explain document:firstdoc writer user:emilia registerImportCmd(rootCmd) registerValidateCmd(rootCmd) registerBackupCmd(rootCmd) - registerPreviewCmd(rootCmd) registerMCPCmd(rootCmd) // Register shared commands. @@ -128,7 +127,8 @@ zed permission check --explain document:firstdoc writer user:emilia commands.RegisterWatchRelationshipCmd(relCmd) schemaCmd := commands.RegisterSchemaCmd(rootCmd) - registerAdditionalSchemaCmds(schemaCmd) + schemaCompileCmd := registerAdditionalSchemaCmds(schemaCmd) + registerPreviewCmd(rootCmd, schemaCompileCmd) return rootCmd } diff --git a/internal/cmd/cmd_test.go b/internal/cmd/cmd_test.go index 69b94925..b9de1a30 100644 --- a/internal/cmd/cmd_test.go +++ b/internal/cmd/cmd_test.go @@ -58,11 +58,11 @@ func TestCommandOutput(t *testing.T) { }, } - zl := cobrazerolog.New(cobrazerolog.WithPreRunLevel(zerolog.DebugLevel)) - - rootCmd := InitialiseRootCmd(zl) for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { + zl := cobrazerolog.New(cobrazerolog.WithPreRunLevel(zerolog.DebugLevel)) + rootCmd := InitialiseRootCmd(zl) + var flagErrorCalled bool testFlagError := func(cmd *cobra.Command, err error) error { require.ErrorContains(t, err, tt.flagErrorContains) @@ -86,6 +86,26 @@ func TestCommandOutput(t *testing.T) { } } +// TestMultipleInitialiseRootCmd is a regression test to ensure that calling +// InitialiseRootCmd multiple times doesn't panic due to flag redefinition. +// This fixes issue #556. +func TestMultipleInitialiseRootCmd(t *testing.T) { + zl := cobrazerolog.New(cobrazerolog.WithPreRunLevel(zerolog.DebugLevel)) + + // Call InitialiseRootCmd multiple times to simulate what happens + // when tests run with -count=10 + for i := 0; i < 10; i++ { + rootCmd := InitialiseRootCmd(zl) + require.NotNil(t, rootCmd) + + // Execute the command with invalid args to trigger the command tree + // This ensures all commands and flags are properly initialized + os.Args = []string{"zed", "version", "--invalid-flag"} + err := rootCmd.ExecuteContext(t.Context()) + require.Error(t, err) // We expect an error due to the invalid flag + } +} + func setupOutputForTest(t *testing.T, testFlagError func(cmd *cobra.Command, err error) error, args ...string) string { t.Helper() diff --git a/internal/cmd/context.go b/internal/cmd/context.go index 766784b0..3cfcb192 100644 --- a/internal/cmd/context.go +++ b/internal/cmd/context.go @@ -16,56 +16,54 @@ import ( ) func registerContextCmd(rootCmd *cobra.Command) { - rootCmd.AddCommand(contextCmd) + contextCmd := &cobra.Command{ + Use: "context ", + Short: "Manage configurations for connecting to SpiceDB deployments", + Aliases: []string{"ctx"}, + } + + contextListCmd := &cobra.Command{ + Use: "list", + Short: "Lists all available contexts", + Aliases: []string{"ls"}, + Args: commands.ValidationWrapper(cobra.ExactArgs(0)), + ValidArgsFunction: cobra.NoFileCompletions, + RunE: contextListCmdFunc, + } + + contextSetCmd := &cobra.Command{ + Use: "set ", + Short: "Creates or overwrite a context", + Args: commands.ValidationWrapper(cobra.ExactArgs(3)), + ValidArgsFunction: cobra.NoFileCompletions, + RunE: contextSetCmdFunc, + } + contextRemoveCmd := &cobra.Command{ + Use: "remove ", + Short: "Removes a context", + Aliases: []string{"rm"}, + Args: commands.ValidationWrapper(cobra.ExactArgs(1)), + ValidArgsFunction: ContextGet, + RunE: contextRemoveCmdFunc, + } + + contextUseCmd := &cobra.Command{ + Use: "use ", + Short: "Sets a context as the current context", + Args: commands.ValidationWrapper(cobra.MaximumNArgs(1)), + ValidArgsFunction: ContextGet, + RunE: contextUseCmdFunc, + } + + rootCmd.AddCommand(contextCmd) contextCmd.AddCommand(contextListCmd) contextListCmd.Flags().Bool("reveal-tokens", false, "display secrets in results") - contextCmd.AddCommand(contextSetCmd) contextCmd.AddCommand(contextRemoveCmd) contextCmd.AddCommand(contextUseCmd) } -var contextCmd = &cobra.Command{ - Use: "context ", - Short: "Manage configurations for connecting to SpiceDB deployments", - Aliases: []string{"ctx"}, -} - -var contextListCmd = &cobra.Command{ - Use: "list", - Short: "Lists all available contexts", - Aliases: []string{"ls"}, - Args: commands.ValidationWrapper(cobra.ExactArgs(0)), - ValidArgsFunction: cobra.NoFileCompletions, - RunE: contextListCmdFunc, -} - -var contextSetCmd = &cobra.Command{ - Use: "set ", - Short: "Creates or overwrite a context", - Args: commands.ValidationWrapper(cobra.ExactArgs(3)), - ValidArgsFunction: cobra.NoFileCompletions, - RunE: contextSetCmdFunc, -} - -var contextRemoveCmd = &cobra.Command{ - Use: "remove ", - Short: "Removes a context", - Aliases: []string{"rm"}, - Args: commands.ValidationWrapper(cobra.ExactArgs(1)), - ValidArgsFunction: ContextGet, - RunE: contextRemoveCmdFunc, -} - -var contextUseCmd = &cobra.Command{ - Use: "use ", - Short: "Sets a context as the current context", - Args: commands.ValidationWrapper(cobra.MaximumNArgs(1)), - ValidArgsFunction: ContextGet, - RunE: contextUseCmdFunc, -} - func ContextGet(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { _, secretStore := client.DefaultStorage() secrets, err := secretStore.Get() diff --git a/internal/cmd/import.go b/internal/cmd/import.go index 687d42ef..872af7a8 100644 --- a/internal/cmd/import.go +++ b/internal/cmd/import.go @@ -22,18 +22,10 @@ import ( ) func registerImportCmd(rootCmd *cobra.Command) { - rootCmd.AddCommand(importCmd) - importCmd.Flags().Int("batch-size", 1000, "import batch size") - importCmd.Flags().Int("workers", 1, "number of concurrent batching workers") - importCmd.Flags().Bool("schema", true, "import schema") - importCmd.Flags().Bool("relationships", true, "import relationships") - importCmd.Flags().String("schema-definition-prefix", "", "prefix to add to the schema's definition(s) before importing") -} - -var importCmd = &cobra.Command{ - Use: "import ", - Short: "Imports schema and relationships from a file or url", - Example: ` + importCmd := &cobra.Command{ + Use: "import ", + Short: "Imports schema and relationships from a file or url", + Example: ` From a gist: zed import https://gist.github.com/ecordell/8e3b613a677e3c844742cf24421c08b6 @@ -61,8 +53,16 @@ var importCmd = &cobra.Command{ With schema definition prefix: zed import --schema-definition-prefix=mypermsystem file:///Users/zed/Downloads/authzed-x7izWU8_2Gw3.yaml `, - Args: commands.ValidationWrapper(cobra.ExactArgs(1)), - RunE: importCmdFunc, + Args: commands.ValidationWrapper(cobra.ExactArgs(1)), + RunE: importCmdFunc, + } + + rootCmd.AddCommand(importCmd) + importCmd.Flags().Int("batch-size", 1000, "import batch size") + importCmd.Flags().Int("workers", 1, "number of concurrent batching workers") + importCmd.Flags().Bool("schema", true, "import schema") + importCmd.Flags().Bool("relationships", true, "import relationships") + importCmd.Flags().String("schema-definition-prefix", "", "prefix to add to the schema's definition(s) before importing") } func importCmdFunc(cmd *cobra.Command, args []string) error { diff --git a/internal/cmd/mcp.go b/internal/cmd/mcp.go index e7ef3331..ac369829 100644 --- a/internal/cmd/mcp.go +++ b/internal/cmd/mcp.go @@ -8,28 +8,27 @@ import ( ) func registerMCPCmd(rootCmd *cobra.Command) { - rootCmd.AddCommand(mcpCmd) - mcpCmd.AddCommand(mcpRunCmd) - - mcpRunCmd.Flags().IntP("port", "p", 9999, "port for the HTTP streaming server") -} + mcpCmd := &cobra.Command{ + Use: "mcp ", + Short: "MCP (Model Context Protocol) server commands", + Long: `MCP (Model Context Protocol) server commands. -var mcpCmd = &cobra.Command{ - Use: "mcp ", - Short: "MCP (Model Context Protocol) server commands", - Long: `MCP (Model Context Protocol) server commands. - The MCP server provides tooling and resources for developing and debugging SpiceDB schema and relationships. The server runs an in-memory development instance of SpiceDB and does not connect to a running instance of SpiceDB. To use with Claude Code, run ` + "`zed mcp experimental-run`" + ` to start the SpiceDB Dev MCP server and then run ` + "`claude mcp add --transport http spicedb \"http://localhost:9999/mcp\"`" + ` to add the server to your Claude Code integrations.`, -} + } + + mcpRunCmd := &cobra.Command{ + Use: "experimental-run", + Short: "Run the Experimental MCP server", + Args: commands.ValidationWrapper(cobra.ExactArgs(0)), + ValidArgsFunction: cobra.NoFileCompletions, + RunE: mcpRunCmdFunc, + } -var mcpRunCmd = &cobra.Command{ - Use: "experimental-run", - Short: "Run the Experimental MCP server", - Args: commands.ValidationWrapper(cobra.ExactArgs(0)), - ValidArgsFunction: cobra.NoFileCompletions, - RunE: mcpRunCmdFunc, + rootCmd.AddCommand(mcpCmd) + mcpCmd.AddCommand(mcpRunCmd) + mcpRunCmd.Flags().IntP("port", "p", 9999, "port for the HTTP streaming server") } func mcpRunCmdFunc(cmd *cobra.Command, _ []string) error { diff --git a/internal/cmd/preview.go b/internal/cmd/preview.go index 7f0230b4..cbe32311 100644 --- a/internal/cmd/preview.go +++ b/internal/cmd/preview.go @@ -4,21 +4,19 @@ import ( "github.com/spf13/cobra" ) -func registerPreviewCmd(rootCmd *cobra.Command) { - rootCmd.AddCommand(previewCmd) +func registerPreviewCmd(rootCmd *cobra.Command, schemaCompileCmd *cobra.Command) { + previewCmd := &cobra.Command{ + Use: "preview ", + Short: "Experimental commands that have been made available for preview", + } - previewCmd.AddCommand(schemaCmd) + schemaCmd := &cobra.Command{ + Use: "schema ", + Short: "Manage schema for a permissions system", + Deprecated: "please use `zed schema compile`", + } + rootCmd.AddCommand(previewCmd) + previewCmd.AddCommand(schemaCmd) schemaCmd.AddCommand(schemaCompileCmd) } - -var previewCmd = &cobra.Command{ - Use: "preview ", - Short: "Experimental commands that have been made available for preview", -} - -var schemaCmd = &cobra.Command{ - Use: "schema ", - Short: "Manage schema for a permissions system", - Deprecated: "please use `zed schema compile`", -} diff --git a/internal/cmd/schema.go b/internal/cmd/schema.go index 27dbf33b..6a5cc292 100644 --- a/internal/cmd/schema.go +++ b/internal/cmd/schema.go @@ -40,7 +40,58 @@ func (rtc *realTermChecker) IsTerminal(fd int) bool { return term.IsTerminal(fd) } -func registerAdditionalSchemaCmds(schemaCmd *cobra.Command) { +func registerAdditionalSchemaCmds(schemaCmd *cobra.Command) *cobra.Command { + schemaWriteCmd := &cobra.Command{ + Use: "write ", + Args: commands.ValidationWrapper(cobra.MaximumNArgs(1)), + Short: "Write a schema file (.zed or stdin) to the current permissions system", + ValidArgsFunction: commands.FileExtensionCompletions("zed"), + Example: ` + Write from a file: + zed schema write schema.zed + Write from stdin: + cat schema.zed | zed schema write +`, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := client.NewClient(cmd) + if err != nil { + return err + } + return schemaWriteCmdImpl(cmd, args, client, &realTermChecker{}) + }, + } + + schemaCopyCmd := &cobra.Command{ + Use: "copy ", + Short: "Copy a schema from one context into another", + Args: commands.ValidationWrapper(cobra.ExactArgs(2)), + ValidArgsFunction: ContextGet, + RunE: schemaCopyCmdFunc, + } + + schemaDiffCmd := &cobra.Command{ + Use: "diff ", + Short: "Diff two schema files", + Args: commands.ValidationWrapper(cobra.ExactArgs(2)), + RunE: schemaDiffCmdFunc, + } + + schemaCompileCmd := &cobra.Command{ + Use: "compile ", + Args: commands.ValidationWrapper(cobra.ExactArgs(1)), + Short: "Compile a schema that uses extended syntax into one that can be written to SpiceDB", + Example: ` + Write to stdout: + zed preview schema compile root.zed + Write to an output file: + zed preview schema compile root.zed --out compiled.zed + `, + ValidArgsFunction: commands.FileExtensionCompletions("zed"), + RunE: func(cmd *cobra.Command, args []string) error { + return schemaCompileCmdFunc(cmd, args, &realTermChecker{}) + }, + } + schemaCmd.AddCommand(schemaCopyCmd) schemaCopyCmd.Flags().Bool("json", false, "output as JSON") schemaCopyCmd.Flags().String("schema-definition-prefix", "", "prefix to add to the schema's definition(s) before writing") @@ -53,57 +104,8 @@ func registerAdditionalSchemaCmds(schemaCmd *cobra.Command) { schemaCmd.AddCommand(schemaCompileCmd) schemaCompileCmd.Flags().String("out", "", "output filepath; omitting writes to stdout") -} - -var schemaWriteCmd = &cobra.Command{ - Use: "write ", - Args: commands.ValidationWrapper(cobra.MaximumNArgs(1)), - Short: "Write a schema file (.zed or stdin) to the current permissions system", - ValidArgsFunction: commands.FileExtensionCompletions("zed"), - Example: ` - Write from a file: - zed schema write schema.zed - Write from stdin: - cat schema.zed | zed schema write -`, - RunE: func(cmd *cobra.Command, args []string) error { - client, err := client.NewClient(cmd) - if err != nil { - return err - } - return schemaWriteCmdImpl(cmd, args, client, &realTermChecker{}) - }, -} - -var schemaCopyCmd = &cobra.Command{ - Use: "copy ", - Short: "Copy a schema from one context into another", - Args: commands.ValidationWrapper(cobra.ExactArgs(2)), - ValidArgsFunction: ContextGet, - RunE: schemaCopyCmdFunc, -} - -var schemaDiffCmd = &cobra.Command{ - Use: "diff ", - Short: "Diff two schema files", - Args: commands.ValidationWrapper(cobra.ExactArgs(2)), - RunE: schemaDiffCmdFunc, -} -var schemaCompileCmd = &cobra.Command{ - Use: "compile ", - Args: commands.ValidationWrapper(cobra.ExactArgs(1)), - Short: "Compile a schema that uses extended syntax into one that can be written to SpiceDB", - Example: ` - Write to stdout: - zed preview schema compile root.zed - Write to an output file: - zed preview schema compile root.zed --out compiled.zed - `, - ValidArgsFunction: commands.FileExtensionCompletions("zed"), - RunE: func(cmd *cobra.Command, args []string) error { - return schemaCompileCmdFunc(cmd, args, &realTermChecker{}) - }, + return schemaCompileCmd } func schemaDiffCmdFunc(_ *cobra.Command, args []string) error { diff --git a/internal/cmd/validate.go b/internal/cmd/validate.go index 4ee2818c..78c8c431 100644 --- a/internal/cmd/validate.go +++ b/internal/cmd/validate.go @@ -48,16 +48,10 @@ var ( ) func registerValidateCmd(cmd *cobra.Command) { - validateCmd.Flags().Bool("force-color", false, "force color code output even in non-tty environments") - validateCmd.Flags().Bool("fail-on-warn", false, "treat warnings as errors during validation") - validateCmd.Flags().String("schema-type", "", "force validation according to specific schema syntax (\"\", \"composable\", \"standard\")") - cmd.AddCommand(validateCmd) -} - -var validateCmd = &cobra.Command{ - Use: "validate ", - Short: "Validates the given validation file (.yaml, .zaml) or schema file (.zed)", - Example: ` + validateCmd := &cobra.Command{ + Use: "validate ", + Short: "Validates the given validation file (.yaml, .zaml) or schema file (.zed)", + Example: ` From a local file (with prefix): zed validate file:///Users/zed/Downloads/authzed-x7izWU8_2Gw3.yaml @@ -75,25 +69,31 @@ var validateCmd = &cobra.Command{ From a devtools instance: zed validate https://localhost:8443/download`, - Args: commands.ValidationWrapper(cobra.MinimumNArgs(1)), - ValidArgsFunction: commands.FileExtensionCompletions("zed", "yaml", "zaml"), - PreRunE: validatePreRunE, - RunE: func(cmd *cobra.Command, filenames []string) error { - result, shouldExit, err := validateCmdFunc(cmd, filenames) - if err != nil { - return err - } - console.Print(result) - if shouldExit { - os.Exit(1) - } - return nil - }, + Args: commands.ValidationWrapper(cobra.MinimumNArgs(1)), + ValidArgsFunction: commands.FileExtensionCompletions("zed", "yaml", "zaml"), + PreRunE: validatePreRunE, + RunE: func(cmd *cobra.Command, filenames []string) error { + result, shouldExit, err := validateCmdFunc(cmd, filenames) + if err != nil { + return err + } + console.Print(result) + if shouldExit { + os.Exit(1) + } + return nil + }, + + // A schema that causes the parser/compiler to error will halt execution + // of this command with an error. In that case, we want to just display the error, + // rather than showing usage for this command. + SilenceUsage: true, + } - // A schema that causes the parser/compiler to error will halt execution - // of this command with an error. In that case, we want to just display the error, - // rather than showing usage for this command. - SilenceUsage: true, + validateCmd.Flags().Bool("force-color", false, "force color code output even in non-tty environments") + validateCmd.Flags().Bool("fail-on-warn", false, "treat warnings as errors during validation") + validateCmd.Flags().String("schema-type", "", "force validation according to specific schema syntax (\"\", \"composable\", \"standard\")") + cmd.AddCommand(validateCmd) } var validSchemaTypes = []string{"", "standard", "composable"} diff --git a/internal/commands/permission.go b/internal/commands/permission.go index d0171631..62bd0c0b 100644 --- a/internal/commands/permission.go +++ b/internal/commands/permission.go @@ -69,6 +69,61 @@ func consistencyFromCmd(cmd *cobra.Command) (c *v1.Consistency, err error) { } func RegisterPermissionCmd(rootCmd *cobra.Command) *cobra.Command { + permissionCmd := &cobra.Command{ + Use: "permission ", + Short: "Query the permissions in a permissions system", + Aliases: []string{"perm"}, + } + + checkBulkCmd := &cobra.Command{ + Use: "bulk ...", + Short: "Check permissions in bulk exist for resource-subject pairs", + Args: ValidationWrapper(cobra.MinimumNArgs(1)), + RunE: checkBulkCmdFunc, + } + + checkCmd := &cobra.Command{ + Use: "check ", + Short: "Check that a permission exists for a subject", + Args: ValidationWrapper(cobra.ExactArgs(3)), + ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectID), + RunE: checkCmdFunc, + } + + expandCmd := &cobra.Command{ + Use: "expand ", + Short: "Expand the structure of a permission", + Args: ValidationWrapper(cobra.ExactArgs(2)), + ValidArgsFunction: cobra.NoFileCompletions, + RunE: expandCmdFunc, + } + + lookupResourcesCmd := &cobra.Command{ + Use: "lookup-resources ", + Short: "Enumerates the resources of a given type for which the subject has permission", + Args: ValidationWrapper(cobra.ExactArgs(3)), + ValidArgsFunction: GetArgs(ResourceType, Permission, SubjectID), + RunE: lookupResourcesCmdFunc, + } + + lookupCmd := &cobra.Command{ + Use: "lookup ", + Short: "Enumerates the resources of a given type for which the subject has permission", + Args: ValidationWrapper(cobra.ExactArgs(3)), + ValidArgsFunction: GetArgs(ResourceType, Permission, SubjectID), + RunE: lookupResourcesCmdFunc, + Deprecated: "prefer lookup-resources", + Hidden: true, + } + + lookupSubjectsCmd := &cobra.Command{ + Use: "lookup-subjects ", + Short: "Enumerates the subjects of a given type for which the subject has permission on the resource", + Args: ValidationWrapper(cobra.ExactArgs(3)), + ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), + RunE: lookupSubjectsCmdFunc, + } + rootCmd.AddCommand(permissionCmd) permissionCmd.AddCommand(checkCmd) @@ -120,61 +175,6 @@ func RegisterPermissionCmd(rootCmd *cobra.Command) *cobra.Command { return permissionCmd } -var permissionCmd = &cobra.Command{ - Use: "permission ", - Short: "Query the permissions in a permissions system", - Aliases: []string{"perm"}, -} - -var checkBulkCmd = &cobra.Command{ - Use: "bulk ...", - Short: "Check permissions in bulk exist for resource-subject pairs", - Args: ValidationWrapper(cobra.MinimumNArgs(1)), - RunE: checkBulkCmdFunc, -} - -var checkCmd = &cobra.Command{ - Use: "check ", - Short: "Check that a permission exists for a subject", - Args: ValidationWrapper(cobra.ExactArgs(3)), - ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectID), - RunE: checkCmdFunc, -} - -var expandCmd = &cobra.Command{ - Use: "expand ", - Short: "Expand the structure of a permission", - Args: ValidationWrapper(cobra.ExactArgs(2)), - ValidArgsFunction: cobra.NoFileCompletions, - RunE: expandCmdFunc, -} - -var lookupResourcesCmd = &cobra.Command{ - Use: "lookup-resources ", - Short: "Enumerates the resources of a given type for which the subject has permission", - Args: ValidationWrapper(cobra.ExactArgs(3)), - ValidArgsFunction: GetArgs(ResourceType, Permission, SubjectID), - RunE: lookupResourcesCmdFunc, -} - -var lookupCmd = &cobra.Command{ - Use: "lookup ", - Short: "Enumerates the resources of a given type for which the subject has permission", - Args: ValidationWrapper(cobra.ExactArgs(3)), - ValidArgsFunction: GetArgs(ResourceType, Permission, SubjectID), - RunE: lookupResourcesCmdFunc, - Deprecated: "prefer lookup-resources", - Hidden: true, -} - -var lookupSubjectsCmd = &cobra.Command{ - Use: "lookup-subjects ", - Short: "Enumerates the subjects of a given type for which the subject has permission on the resource", - Args: ValidationWrapper(cobra.ExactArgs(3)), - ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), - RunE: lookupSubjectsCmdFunc, -} - func checkCmdFunc(cmd *cobra.Command, args []string) error { var objectNS, objectID string err := stringz.SplitExact(args[0], ":", &objectNS, &objectID) diff --git a/internal/commands/relationship.go b/internal/commands/relationship.go index 0471f856..9af24600 100644 --- a/internal/commands/relationship.go +++ b/internal/commands/relationship.go @@ -26,7 +26,60 @@ import ( "github.com/authzed/zed/internal/console" ) +const readCmdHelpLong = `Enumerates relationships matching the provided pattern. + +To filter returned relationships using a resource ID prefix, append a '%' to the resource ID: + +zed relationship read some-type:some-prefix-% +` + func RegisterRelationshipCmd(rootCmd *cobra.Command) *cobra.Command { + relationshipCmd := &cobra.Command{ + Use: "relationship ", + Short: "Query and mutate the relationships in a permissions system", + } + + createCmd := &cobra.Command{ + Use: "create ", + Short: "Create a relationship for a subject", + Args: ValidationWrapper(StdinOrExactArgs(3)), + ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), + RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_CREATE, os.Stdin), + } + + touchCmd := &cobra.Command{ + Use: "touch ", + Short: "Idempotently updates a relationship for a subject", + Args: ValidationWrapper(StdinOrExactArgs(3)), + ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), + RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_TOUCH, os.Stdin), + } + + deleteCmd := &cobra.Command{ + Use: "delete ", + Short: "Deletes a relationship", + Args: ValidationWrapper(StdinOrExactArgs(3)), + ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), + RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_DELETE, os.Stdin), + } + + readCmd := &cobra.Command{ + Use: "read ", + Short: "Enumerates relationships matching the provided pattern", + Long: readCmdHelpLong, + Args: ValidationWrapper(cobra.RangeArgs(1, 3)), + ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), + RunE: readRelationships, + } + + bulkDeleteCmd := &cobra.Command{ + Use: "bulk-delete ", + Short: "Deletes relationships matching the provided pattern en masse", + Args: ValidationWrapper(cobra.RangeArgs(1, 3)), + ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), + RunE: bulkDeleteRelationships, + } + rootCmd.AddCommand(relationshipCmd) relationshipCmd.AddCommand(createCmd) @@ -62,59 +115,6 @@ func RegisterRelationshipCmd(rootCmd *cobra.Command) *cobra.Command { return relationshipCmd } -var relationshipCmd = &cobra.Command{ - Use: "relationship ", - Short: "Query and mutate the relationships in a permissions system", -} - -var createCmd = &cobra.Command{ - Use: "create ", - Short: "Create a relationship for a subject", - Args: ValidationWrapper(StdinOrExactArgs(3)), - ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), - RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_CREATE, os.Stdin), -} - -var touchCmd = &cobra.Command{ - Use: "touch ", - Short: "Idempotently updates a relationship for a subject", - Args: ValidationWrapper(StdinOrExactArgs(3)), - ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), - RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_TOUCH, os.Stdin), -} - -var deleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Deletes a relationship", - Args: ValidationWrapper(StdinOrExactArgs(3)), - ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), - RunE: writeRelationshipCmdFunc(v1.RelationshipUpdate_OPERATION_DELETE, os.Stdin), -} - -const readCmdHelpLong = `Enumerates relationships matching the provided pattern. - -To filter returned relationships using a resource ID prefix, append a '%' to the resource ID: - -zed relationship read some-type:some-prefix-% -` - -var readCmd = &cobra.Command{ - Use: "read ", - Short: "Enumerates relationships matching the provided pattern", - Long: readCmdHelpLong, - Args: ValidationWrapper(cobra.RangeArgs(1, 3)), - ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), - RunE: readRelationships, -} - -var bulkDeleteCmd = &cobra.Command{ - Use: "bulk-delete ", - Short: "Deletes relationships matching the provided pattern en masse", - Args: ValidationWrapper(cobra.RangeArgs(1, 3)), - ValidArgsFunction: GetArgs(ResourceID, Permission, SubjectTypeWithOptionalRelation), - RunE: bulkDeleteRelationships, -} - func StdinOrExactArgs(n int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if ok := isArgsViaFile(os.Stdin) && len(args) == 0; ok { diff --git a/internal/commands/schema.go b/internal/commands/schema.go index cb020747..e810ef46 100644 --- a/internal/commands/schema.go +++ b/internal/commands/schema.go @@ -17,28 +17,25 @@ import ( ) func RegisterSchemaCmd(rootCmd *cobra.Command) *cobra.Command { - rootCmd.AddCommand(schemaCmd) - - schemaCmd.AddCommand(schemaReadCmd) - schemaReadCmd.Flags().Bool("json", false, "output as JSON") - - return schemaCmd -} - -var ( - schemaCmd = &cobra.Command{ + schemaCmd := &cobra.Command{ Use: "schema ", Short: "Manage schema for a permissions system", } - schemaReadCmd = &cobra.Command{ + schemaReadCmd := &cobra.Command{ Use: "read", Short: "Read the schema of a permissions system", Args: ValidationWrapper(cobra.ExactArgs(0)), ValidArgsFunction: cobra.NoFileCompletions, RunE: schemaReadCmdFunc, } -) + + rootCmd.AddCommand(schemaCmd) + schemaCmd.AddCommand(schemaReadCmd) + schemaReadCmd.Flags().Bool("json", false, "output as JSON") + + return schemaCmd +} func schemaReadCmdFunc(cmd *cobra.Command, _ []string) error { client, err := client.NewClient(cmd) diff --git a/internal/commands/watch.go b/internal/commands/watch.go index 1e0313e9..1ff13e31 100644 --- a/internal/commands/watch.go +++ b/internal/commands/watch.go @@ -28,8 +28,15 @@ var ( ) func RegisterWatchCmd(rootCmd *cobra.Command) *cobra.Command { - rootCmd.AddCommand(watchCmd) + watchCmd := &cobra.Command{ + Use: "watch [object_types, ...] [start_cursor]", + Short: "Watches the stream of relationship updates and schema updates from the server", + Args: ValidationWrapper(cobra.RangeArgs(0, 2)), + RunE: watchCmdFunc, + Deprecated: "please use `zed relationships watch` instead", + } + rootCmd.AddCommand(watchCmd) watchCmd.Flags().StringSliceVar(&watchObjectTypes, "object_types", nil, "optional object types to watch updates for") watchCmd.Flags().StringVar(&watchRevision, "revision", "", "optional revision at which to start watching") watchCmd.Flags().BoolVar(&watchTimestamps, "timestamp", false, "shows timestamp of incoming update events") @@ -37,6 +44,13 @@ func RegisterWatchCmd(rootCmd *cobra.Command) *cobra.Command { } func RegisterWatchRelationshipCmd(parentCmd *cobra.Command) *cobra.Command { + watchRelationshipsCmd := &cobra.Command{ + Use: "watch [object_types, ...] [start_cursor]", + Short: "Watches the stream of relationship updates and schema updates from the server", + Args: ValidationWrapper(cobra.RangeArgs(0, 2)), + RunE: watchCmdFunc, + } + parentCmd.AddCommand(watchRelationshipsCmd) watchRelationshipsCmd.Flags().StringSliceVar(&watchObjectTypes, "object_types", nil, "optional object types to watch updates for") watchRelationshipsCmd.Flags().StringVar(&watchRevision, "revision", "", "optional revision at which to start watching") @@ -45,21 +59,6 @@ func RegisterWatchRelationshipCmd(parentCmd *cobra.Command) *cobra.Command { return watchRelationshipsCmd } -var watchCmd = &cobra.Command{ - Use: "watch [object_types, ...] [start_cursor]", - Short: "Watches the stream of relationship updates and schema updates from the server", - Args: ValidationWrapper(cobra.RangeArgs(0, 2)), - RunE: watchCmdFunc, - Deprecated: "please use `zed relationships watch` instead", -} - -var watchRelationshipsCmd = &cobra.Command{ - Use: "watch [object_types, ...] [start_cursor]", - Short: "Watches the stream of relationship updates and schema updates from the server", - Args: ValidationWrapper(cobra.RangeArgs(0, 2)), - RunE: watchCmdFunc, -} - func watchCmdFunc(cmd *cobra.Command, _ []string) error { client, err := client.NewClient(cmd) if err != nil {