From 3aeff47a94991e4727f9aa981ed8313c63fdaa7c Mon Sep 17 00:00:00 2001 From: Lee Tschetter Date: Sat, 19 Sep 2026 02:21:02 +0000 Subject: [PATCH] feat: add opt-in task mutation confirmations Co-authored-by: c1-squire-dev[bot] --- README.md | 7 ++++++ cmd/cone/confirmation.go | 46 +++++++++++++++++++++++++++++++++++ cmd/cone/confirmation_test.go | 28 +++++++++++++++++++++ cmd/cone/main.go | 8 ++++++ cmd/cone/task_approve_deny.go | 12 ++++++--- cmd/cone/task_comment.go | 12 ++++++--- cmd/cone/task_escalate.go | 12 ++++++--- 7 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 cmd/cone/confirmation.go create mode 100644 cmd/cone/confirmation_test.go diff --git a/README.md b/README.md index c399b8a0..bd386f2a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,13 @@ $ docker pull public.ecr.aws/conductorone/cone: To authenticate to Cone, run `cone login `, passing in the name (such as `example.conductor.one`) or URL (such as `https://example.conductor.one`) of your ConductorOne instance and follow the prompts. +# Confirming task mutations + +Add `--confirm` to `task approve`, `task deny`, `task comment`, or `task +escalate` to require an explicit interactive confirmation after Cone has +resolved and validated the task, immediately before it sends the change. It is +rejected with `--non-interactive`; the default scripting behavior is unchanged. + # Getting started with Cone Run `cone help` to see the full list of available Cone commands. diff --git a/cmd/cone/confirmation.go b/cmd/cone/confirmation.go new file mode 100644 index 00000000..e304c873 --- /dev/null +++ b/cmd/cone/confirmation.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +const ( + confirmFlag = "confirm" + confirmAnnotation = "cone.conductorone.com/confirm" +) + +func supportsConfirmation(cmd *cobra.Command) *cobra.Command { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + cmd.Annotations[confirmAnnotation] = "true" + return cmd +} + +func confirmationSupported(cmd *cobra.Command) bool { + return cmd.Annotations[confirmAnnotation] == "true" +} + +func confirmMutation(cmd *cobra.Command, action string) error { + enabled, _ := cmd.Flags().GetBool(confirmFlag) + if !enabled { + return nil + } + + nonInteractive, _ := cmd.Flags().GetBool(nonInteractiveFlag) + if nonInteractive { + return fmt.Errorf("--confirm requires interactive mode") + } + + confirmed, err := pterm.DefaultInteractiveConfirm.Show("Proceed with " + action + "?") + if err != nil { + return err + } + if !confirmed { + return fmt.Errorf("mutation cancelled") + } + return nil +} diff --git a/cmd/cone/confirmation_test.go b/cmd/cone/confirmation_test.go new file mode 100644 index 00000000..02ca1725 --- /dev/null +++ b/cmd/cone/confirmation_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestConfirmMutationRequiresInteractiveMode(t *testing.T) { + cmd := &cobra.Command{} + cmd.Flags().Bool(confirmFlag, false, "") + cmd.Flags().Bool(nonInteractiveFlag, false, "") + if err := cmd.ParseFlags([]string{"--confirm", "--non-interactive"}); err != nil { + t.Fatalf("ParseFlags: %v", err) + } + + err := confirmMutation(cmd, "creating an access request") + if err == nil || !strings.Contains(err.Error(), "requires interactive mode") { + t.Fatalf("confirmMutation error = %v, want interactive-mode error", err) + } +} + +func TestConfirmMutationIsOptIn(t *testing.T) { + if err := confirmMutation(&cobra.Command{}, "creating an access request"); err != nil { + t.Fatalf("confirmMutation without --confirm: %v", err) + } +} diff --git a/cmd/cone/main.go b/cmd/cone/main.go index 89de04f8..60744d2d 100644 --- a/cmd/cone/main.go +++ b/cmd/cone/main.go @@ -42,7 +42,14 @@ func runCli(ctx context.Context) int { Use: "cone", Short: "Cone interacts with the ConductorOne API to manage access to entitlements.", Version: version, + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + confirm, _ := cmd.Flags().GetBool(confirmFlag) + if confirm && !confirmationSupported(cmd) { + return fmt.Errorf("--confirm is not supported by %q", cmd.CommandPath()) + } cmd.SetContext(ctx) return nil }, @@ -58,6 +65,7 @@ func runCli(ctx context.Context) int { cliCmd.PersistentFlags().StringP("output", "o", "table", "Output format. Valid values: table, json, json-pretty, wide.") cliCmd.PersistentFlags().Bool("debug", false, "Enable HTTP debug logging") cliCmd.PersistentFlags().String("log-level", "", "Set log level (debug, info, warn, error)") + cliCmd.PersistentFlags().Bool(confirmFlag, false, "Prompt before supported task mutations") err := initConfig(cliCmd) if err != nil { diff --git a/cmd/cone/task_approve_deny.go b/cmd/cone/task_approve_deny.go index c86f1d0b..abad9929 100644 --- a/cmd/cone/task_approve_deny.go +++ b/cmd/cone/task_approve_deny.go @@ -20,7 +20,7 @@ func approveTasksCmd() *cobra.Command { addCommentFlag(cmd) addWaitFlag(cmd) - return cmd + return supportsConfirmation(cmd) } func denyTasksCmd() *cobra.Command { @@ -32,11 +32,11 @@ func denyTasksCmd() *cobra.Command { addCommentFlag(cmd) addWaitFlag(cmd) - return cmd + return supportsConfirmation(cmd) } func runApproveTasks(cmd *cobra.Command, args []string) error { - return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error) { + return runApproveDeny(cmd, args, "approving", func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error) { approveResp, err := c.ApproveTask(ctx, taskId, comment, policyId) if err != nil { return nil, err @@ -46,7 +46,7 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { } func runDenyTasks(cmd *cobra.Command, args []string) error { - return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error) { + return runApproveDeny(cmd, args, "denying", func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error) { approveResp, err := c.DenyTask(ctx, taskId, comment, policyId) if err != nil { return nil, err @@ -58,6 +58,7 @@ func runDenyTasks(cmd *cobra.Command, args []string) error { func runApproveDeny( cmd *cobra.Command, args []string, + action string, run func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error), ) error { ctx, c, v, err := cmdContext(cmd) @@ -80,6 +81,9 @@ func runApproveDeny( if taskResp.TaskView.Task.Policy == nil || taskResp.TaskView.Task.Policy.Current == nil { return errors.New("task does not have a current policy step id and cannot be approved or denied") } + if err := confirmMutation(cmd, action+" task "+taskId); err != nil { + return err + } task, err := run(c, ctx, taskId, comment, client.StringFromPtr(taskResp.TaskView.Task.Policy.Current.ID)) if err != nil { diff --git a/cmd/cone/task_comment.go b/cmd/cone/task_comment.go index 07a793ff..7fbffcfb 100644 --- a/cmd/cone/task_comment.go +++ b/cmd/cone/task_comment.go @@ -16,7 +16,7 @@ func tasksCommentCmd() *cobra.Command { RunE: tasksCommentRun, } - return cmd + return supportsConfirmation(cmd) } func tasksCommentRun(cmd *cobra.Command, args []string) error { @@ -29,10 +29,16 @@ func tasksCommentRun(cmd *cobra.Command, args []string) error { return err } - taskId := args[0] + taskID := args[0] comment := args[1] + if _, err := c.GetTask(ctx, taskID); err != nil { + return err + } + if err := confirmMutation(cmd, "commenting on task "+taskID); err != nil { + return err + } - userResp, err := c.CommentOnTask(ctx, taskId, comment) + userResp, err := c.CommentOnTask(ctx, taskID, comment) if err != nil { return err } diff --git a/cmd/cone/task_escalate.go b/cmd/cone/task_escalate.go index a582d52c..7b280223 100644 --- a/cmd/cone/task_escalate.go +++ b/cmd/cone/task_escalate.go @@ -15,7 +15,7 @@ func escalateTasksCmd() *cobra.Command { Short: "Escalate an access request task to emergency access", RunE: runEscalateTasks, } - return cmd + return supportsConfirmation(cmd) } func runEscalateTasks(cmd *cobra.Command, args []string) error { @@ -28,9 +28,15 @@ func runEscalateTasks(cmd *cobra.Command, args []string) error { return err } - taskId := args[0] + taskID := args[0] + if _, err := c.GetTask(ctx, taskID); err != nil { + return err + } + if err := confirmMutation(cmd, "escalating task "+taskID+" to emergency access"); err != nil { + return err + } - userResp, err := c.EscalateTask(ctx, taskId) + userResp, err := c.EscalateTask(ctx, taskID) if err != nil { return err }