Skip to content

Commit 712b2a7

Browse files
committed
User approval wording
1 parent eadedb5 commit 712b2a7

5 files changed

Lines changed: 84 additions & 12 deletions

File tree

aidocs/ELEVATION_MODEL.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,10 @@ Behavior:
229229
- `PluginApprovers.<pipeName>` supersedes `FallbackApprovers` when present.
230230
- The requester is excluded from the eligible approver list, so users cannot
231231
approve their own elevation.
232-
- The requester receives a lowercase-letter menu such as
233-
`a) david, b) bob, c) alice` and must reply with a single lowercase letter
234-
matched by the elevator's `approvalChoice` reply matcher.
232+
- The requester receives a lowercase-letter menu identifying the action as
233+
`plugin/command` when a plugin command is known, such as `vpn/add-device`,
234+
and must reply with a single lowercase letter matched by the elevator's
235+
`approvalChoice` reply matcher.
235236
- The selected approver receives a DM yes/no prompt. `yes`/`y` approves and
236237
returns `robot.Success`; `no`/`n`, timeout, invalid requester choice, or
237238
missing eligible approvers fail elevation.

bot/builtin_userapproval.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type userApprovalRuntime interface {
2222
Say(msg string, v ...interface{}) robot.RetVal
2323
Log(l robot.LogLevel, m string, v ...interface{}) bool
2424
pipelineNameForApproval() string
25+
commandNameForApproval() string
2526
}
2627

2728
func init() {
@@ -32,6 +33,10 @@ func (r Robot) pipelineNameForApproval() string {
3233
return r.pipeName
3334
}
3435

36+
func (r Robot) commandNameForApproval() string {
37+
return r.plugCommand
38+
}
39+
3540
func userApprovalElevate(gr robot.Robot, command string, args ...string) robot.TaskRetVal {
3641
if command != "_elevate" {
3742
return robot.Normal
@@ -52,6 +57,7 @@ func runUserApproval(r userApprovalRuntime) robot.TaskRetVal {
5257

5358
requester := canonicalApprovalUser(r.GetMessage().User)
5459
pipeName := strings.TrimSpace(r.pipelineNameForApproval())
60+
actionName := userApprovalActionName(pipeName, r.commandNameForApproval())
5561
approvers := effectiveApprovalApprovers(cfg, pipeName, requester)
5662
if len(approvers) == 0 {
5763
r.Log(robot.Error, "builtin-userapproval has no eligible approvers for pipeline '%s', requester '%s'", pipeName, requester)
@@ -64,7 +70,7 @@ func runUserApproval(r userApprovalRuntime) robot.TaskRetVal {
6470
return robot.Fail
6571
}
6672

67-
choicePrompt := userApprovalChoicePrompt(pipeName, approvers)
73+
choicePrompt := userApprovalChoicePrompt(actionName, approvers)
6874
choice, ret := r.PromptForReply(userApprovalChoiceMatcher, choicePrompt)
6975
if ret != robot.Ok {
7076
r.Log(robot.Warn, "builtin-userapproval requester '%s' did not select an approver for pipeline '%s': %s", requester, pipeName, ret)
@@ -78,8 +84,8 @@ func runUserApproval(r userApprovalRuntime) robot.TaskRetVal {
7884
}
7985

8086
answer, ret := r.PromptUserForReply("YesNo", approver,
81-
"%s requests approval to run elevated action for '%s'. Reply yes to approve or no to deny.",
82-
requester, pipeName)
87+
"%s is requesting approval to run command %s - approve? (y/n)",
88+
requester, actionName)
8389
if ret != robot.Ok {
8490
r.Log(robot.Warn, "builtin-userapproval approver '%s' did not respond for requester '%s', pipeline '%s': %s", approver, requester, pipeName, ret)
8591
r.Say("Approval request to %s did not complete", approver)
@@ -123,12 +129,24 @@ func canonicalApprovalUser(user string) string {
123129
return strings.ToLower(strings.TrimSpace(user))
124130
}
125131

126-
func userApprovalChoicePrompt(pipeName string, approvers []string) string {
132+
func userApprovalActionName(pipeName, command string) string {
133+
pipeName = strings.TrimSpace(pipeName)
134+
command = strings.TrimSpace(command)
135+
if pipeName == "" {
136+
return command
137+
}
138+
if command == "" {
139+
return pipeName
140+
}
141+
return pipeName + "/" + command
142+
}
143+
144+
func userApprovalChoicePrompt(actionName string, approvers []string) string {
127145
choices := make([]string, 0, len(approvers))
128146
for i, approver := range approvers {
129147
choices = append(choices, fmt.Sprintf("%c) %s", 'a'+i, approver))
130148
}
131-
return fmt.Sprintf("This command requires approval for '%s'. Select one approver: %s", pipeName, strings.Join(choices, ", "))
149+
return fmt.Sprintf("Approval required for command %s. Select one approver: %s", actionName, strings.Join(choices, ", "))
132150
}
133151

134152
func userApprovalApproverForChoice(choice string, approvers []string) (string, bool) {

bot/builtin_userapproval_test.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,30 @@ func TestUserApprovalChoiceMapping(t *testing.T) {
4848
}
4949

5050
func TestUserApprovalChoicePrompt(t *testing.T) {
51-
got := userApprovalChoicePrompt("wireguard", []string{"david", "bob", "alice"})
52-
want := "This command requires approval for 'wireguard'. Select one approver: a) david, b) bob, c) alice"
51+
got := userApprovalChoicePrompt("wireguard/add-device", []string{"david", "bob", "alice"})
52+
want := "Approval required for command wireguard/add-device. Select one approver: a) david, b) bob, c) alice"
5353
if got != want {
5454
t.Fatalf("userApprovalChoicePrompt() = %q, want %q", got, want)
5555
}
5656
}
57+
58+
func TestUserApprovalActionName(t *testing.T) {
59+
for _, tc := range []struct {
60+
name string
61+
pipeName string
62+
command string
63+
want string
64+
}{
65+
{"plugin command", "vpn", "add-device", "vpn/add-device"},
66+
{"pipeline fallback", "maintenance", "", "maintenance"},
67+
{"command fallback", "", "approve", "approve"},
68+
{"trimmed", " vpn ", " add-device ", "vpn/add-device"},
69+
} {
70+
t.Run(tc.name, func(t *testing.T) {
71+
got := userApprovalActionName(tc.pipeName, tc.command)
72+
if got != tc.want {
73+
t.Fatalf("userApprovalActionName(%q, %q) = %q, want %q", tc.pipeName, tc.command, got, tc.want)
74+
}
75+
})
76+
}
77+
}

bot/robot.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func (w *worker) makeRobot() Robot {
8686
pipeName: w.pipeName,
8787
pipeDesc: w.pipeDesc,
8888
taskName: w.taskName,
89+
plugCommand: w.plugCommand,
8990
currentTask: w.currentTask,
9091
exclusive: w.exclusive,
9192
exclusiveTag: w.exclusiveTag,

integration/suites/data/TestUserApprovalElevation.yaml

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ flow:
2121
receive:
2222
user: alice
2323
channel: general
24-
text_pattern: "This command requires approval for 'gosec'\\. Select one approver: a\\) bob, b\\) carol"
24+
text_pattern: "Approval required for command gosec/secelevated\\. Select one approver: a\\) bob, b\\) carol"
2525
- name: select-carol
2626
send:
2727
user: alice
@@ -30,7 +30,7 @@ flow:
3030
- name: carol-approval-prompt
3131
receive:
3232
user: carol
33-
text_pattern: "alice requests approval to run elevated action for 'gosec'\\. Reply yes to approve or no to deny\\."
33+
text_pattern: "alice is requesting approval to run command gosec/secelevated - approve\\? \\(y/n\\)"
3434
- name: carol-approves
3535
send:
3636
user: carol
@@ -43,3 +43,34 @@ flow:
4343
receive:
4444
channel: general
4545
text_pattern: "SECURITY CHECK: secelevated"
46+
- name: request-denied-elevation
47+
send:
48+
user: alice
49+
channel: general
50+
text: ;go-sec-elevated
51+
- name: requester-chooses-denying-approver
52+
receive:
53+
user: alice
54+
channel: general
55+
text_pattern: "Approval required for command gosec/secelevated\\. Select one approver: a\\) bob, b\\) carol"
56+
- name: select-bob
57+
send:
58+
user: alice
59+
channel: general
60+
text: a
61+
- name: bob-approval-prompt
62+
receive:
63+
user: bob
64+
text_pattern: "alice is requesting approval to run command gosec/secelevated - approve\\? \\(y/n\\)"
65+
- name: bob-denies
66+
send:
67+
user: bob
68+
text: "no"
69+
- name: approval-denied
70+
receive:
71+
channel: general
72+
text_pattern: "Approval denied by bob"
73+
- name: protected-command-blocked
74+
receive:
75+
channel: general
76+
text_pattern: "Sorry, this command requires elevation"

0 commit comments

Comments
 (0)