diff --git a/cmd/secrets/common/handler.go b/cmd/secrets/common/handler.go index d409cad5..48975140 100644 --- a/cmd/secrets/common/handler.go +++ b/cmd/secrets/common/handler.go @@ -351,7 +351,7 @@ func (h *Handler) Execute( inputs UpsertSecretsInputs, method string, duration time.Duration, - ownerType string, + secretsAuth string, ) error { ui.Dim("Verifying ownership...") if err := h.EnsureOwnerLinkedOrFail(); err != nil { diff --git a/cmd/secrets/common/validate.go b/cmd/secrets/common/validate.go new file mode 100644 index 00000000..ce7632c2 --- /dev/null +++ b/cmd/secrets/common/validate.go @@ -0,0 +1,32 @@ +package common + +import ( + "fmt" + "strings" +) + +const ( + SecretsAuthOwnerKeySigning = "owner-key-signing" + SecretsAuthBrowser = "browser" +) + +// ValidateSecretsAuthFlow checks that the chosen auth flow is valid and +// allowed in the current environment. Browser flow is blocked in production. +func ValidateSecretsAuthFlow(flow, envName string) error { + switch flow { + case SecretsAuthOwnerKeySigning: + return nil + case SecretsAuthBrowser: + if strings.EqualFold(envName, "PRODUCTION") || envName == "" { + return fmt.Errorf("browser auth flow is not yet available in production; use owner-key-signing") + } + return nil + default: + return fmt.Errorf("unknown --secrets-auth value %q; expected %q or %q", flow, SecretsAuthOwnerKeySigning, SecretsAuthBrowser) + } +} + +// IsBrowserFlow returns true when the browser (JWT) auth flow is selected. +func IsBrowserFlow(flow string) bool { + return flow == SecretsAuthBrowser +} diff --git a/cmd/secrets/common/validate_test.go b/cmd/secrets/common/validate_test.go new file mode 100644 index 00000000..4784732f --- /dev/null +++ b/cmd/secrets/common/validate_test.go @@ -0,0 +1,49 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateSecretsAuthFlow(t *testing.T) { + tests := []struct { + name string + flow string + env string + wantErr bool + errMsg string + }{ + {"owner-key-signing in production", SecretsAuthOwnerKeySigning, "PRODUCTION", false, ""}, + {"owner-key-signing in staging", SecretsAuthOwnerKeySigning, "STAGING", false, ""}, + {"owner-key-signing in dev", SecretsAuthOwnerKeySigning, "DEVELOPMENT", false, ""}, + {"owner-key-signing empty env defaults safe", SecretsAuthOwnerKeySigning, "", false, ""}, + {"browser in staging", SecretsAuthBrowser, "STAGING", false, ""}, + {"browser in dev", SecretsAuthBrowser, "DEVELOPMENT", false, ""}, + {"browser in production blocked", SecretsAuthBrowser, "PRODUCTION", true, "not yet available in production"}, + {"browser in production lowercase", SecretsAuthBrowser, "production", true, "not yet available in production"}, + {"browser empty env treated as production", SecretsAuthBrowser, "", true, "not yet available in production"}, + {"unknown value rejected", "magic", "STAGING", true, "unknown --secrets-auth value"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSecretsAuthFlow(tt.flow, tt.env) + if tt.wantErr { + require.Error(t, err) + if tt.errMsg != "" { + require.Contains(t, err.Error(), tt.errMsg) + } + } else { + require.NoError(t, err) + } + }) + } +} + +func TestIsBrowserFlow(t *testing.T) { + assert.False(t, IsBrowserFlow(SecretsAuthOwnerKeySigning), "owner-key-signing should not be browser flow") + assert.True(t, IsBrowserFlow(SecretsAuthBrowser), "browser should be browser flow") + assert.False(t, IsBrowserFlow("unknown"), "unknown should not be browser flow") +} diff --git a/cmd/secrets/create/create.go b/cmd/secrets/create/create.go index 4ba3327c..ddfd4f45 100644 --- a/cmd/secrets/create/create.go +++ b/cmd/secrets/create/create.go @@ -24,6 +24,14 @@ func New(ctx *runtime.Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { secretsFilePath := args[0] + secretsAuth, err := cmd.Flags().GetString("secrets-auth") + if err != nil { + return err + } + if err := common.ValidateSecretsAuthFlow(secretsAuth, ctx.EnvironmentSet.EnvName); err != nil { + return err + } + h, err := common.NewHandler(ctx, secretsFilePath) if err != nil { return err @@ -54,7 +62,7 @@ func New(ctx *runtime.Context) *cobra.Command { return err } - return h.Execute(inputs, vaulttypes.MethodSecretsCreate, duration, ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType) + return h.Execute(inputs, vaulttypes.MethodSecretsCreate, duration, secretsAuth) }, } diff --git a/cmd/secrets/delete/delete.go b/cmd/secrets/delete/delete.go index 46d63b8b..573c64c2 100644 --- a/cmd/secrets/delete/delete.go +++ b/cmd/secrets/delete/delete.go @@ -57,6 +57,14 @@ func New(ctx *runtime.Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { secretsFilePath := args[0] + secretsAuth, err := cmd.Flags().GetString("secrets-auth") + if err != nil { + return err + } + if err := common.ValidateSecretsAuthFlow(secretsAuth, ctx.EnvironmentSet.EnvName); err != nil { + return err + } + h, err := common.NewHandler(ctx, secretsFilePath) if err != nil { return err @@ -78,7 +86,6 @@ func New(ctx *runtime.Context) *cobra.Command { return fmt.Errorf("invalid --timeout: must be greater than 0 and less than %dh (%dd)", maxHours, maxDays) } - // Parse & validate YAML input inputs, err := ResolveDeleteInputs(secretsFilePath) if err != nil { return err @@ -87,8 +94,7 @@ func New(ctx *runtime.Context) *cobra.Command { return err } - // Two-path logic: MSIG step 1 (bundle) or EOA (allowlist + post) - return Execute(h, inputs, duration, ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType) + return Execute(h, inputs, duration, secretsAuth) }, } @@ -101,7 +107,7 @@ func New(ctx *runtime.Context) *cobra.Command { // Two paths: // - MSIG step 1: build request, compute digest, write bundle, print steps // - EOA: allowlist if needed, then POST to gateway -func Execute(h *common.Handler, inputs DeleteSecretsInputs, duration time.Duration, ownerType string) error { +func Execute(h *common.Handler, inputs DeleteSecretsInputs, duration time.Duration, secretsAuth string) error { spinner := ui.NewSpinner() spinner.Start("Verifying ownership...") if err := h.EnsureOwnerLinkedOrFail(); err != nil { diff --git a/cmd/secrets/list/list.go b/cmd/secrets/list/list.go index f9c3e433..ba15e056 100644 --- a/cmd/secrets/list/list.go +++ b/cmd/secrets/list/list.go @@ -36,6 +36,14 @@ func New(ctx *runtime.Context) *cobra.Command { Use: "list", Short: "Lists secret identifiers for the current owner address in the given namespace.", RunE: func(cmd *cobra.Command, args []string) error { + secretsAuth, err := cmd.Flags().GetString("secrets-auth") + if err != nil { + return err + } + if err := common.ValidateSecretsAuthFlow(secretsAuth, ctx.EnvironmentSet.EnvName); err != nil { + return err + } + h, err := common.NewHandler(ctx, "") if err != nil { return err @@ -58,12 +66,7 @@ func New(ctx *runtime.Context) *cobra.Command { return fmt.Errorf("invalid --timeout: must be greater than 0 and less than %dh (%dd)", maxHours, maxDays) } - return Execute( - h, - namespace, - duration, - ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType, - ) + return Execute(h, namespace, duration, secretsAuth) }, } @@ -75,7 +78,7 @@ func New(ctx *runtime.Context) *cobra.Command { } // Execute performs: build request → (MSIG step 1 bundle OR EOA allowlist+post) → parse. -func Execute(h *common.Handler, namespace string, duration time.Duration, ownerType string) error { +func Execute(h *common.Handler, namespace string, duration time.Duration, secretsAuth string) error { spinner := ui.NewSpinner() spinner.Start("Verifying ownership...") if err := h.EnsureOwnerLinkedOrFail(); err != nil { diff --git a/cmd/secrets/secrets.go b/cmd/secrets/secrets.go index db11100d..8ad618c9 100644 --- a/cmd/secrets/secrets.go +++ b/cmd/secrets/secrets.go @@ -32,6 +32,9 @@ func New(runtimeContext *runtime.Context) *cobra.Command { "Timeout for secrets operations (e.g. 30m, 2h, 48h).", ) + secretsCmd.PersistentFlags().String("secrets-auth", "owner-key-signing", "Auth flow for secrets operations (owner-key-signing, browser).") + _ = secretsCmd.PersistentFlags().MarkHidden("secrets-auth") + secretsCmd.AddCommand(create.New(runtimeContext)) secretsCmd.AddCommand(update.New(runtimeContext)) secretsCmd.AddCommand(delete.New(runtimeContext)) diff --git a/cmd/secrets/update/update.go b/cmd/secrets/update/update.go index c7cbfd82..95019d27 100644 --- a/cmd/secrets/update/update.go +++ b/cmd/secrets/update/update.go @@ -24,6 +24,14 @@ func New(ctx *runtime.Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { secretsFilePath := args[0] + secretsAuth, err := cmd.Flags().GetString("secrets-auth") + if err != nil { + return err + } + if err := common.ValidateSecretsAuthFlow(secretsAuth, ctx.EnvironmentSet.EnvName); err != nil { + return err + } + h, err := common.NewHandler(ctx, secretsFilePath) if err != nil { return err @@ -55,12 +63,7 @@ func New(ctx *runtime.Context) *cobra.Command { return err } - return h.Execute( - inputs, - vaulttypes.MethodSecretsUpdate, - duration, - ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType, - ) + return h.Execute(inputs, vaulttypes.MethodSecretsUpdate, duration, secretsAuth) }, }