Skip to content

Commit 5c64269

Browse files
committed
feat(slack): Allow trusted workflows to execute (as impersonation)
1 parent 09a9e93 commit 5c64269

File tree

3 files changed

+55
-6
lines changed

3 files changed

+55
-6
lines changed

pkg/bot/command.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package bot
33
import (
44
"bytes"
55
"fmt"
6+
"log/slog"
67
"os"
78
"os/exec"
89
"slices"
@@ -148,6 +149,7 @@ func compileCommands(templates map[string]string, cc []*config.CommandConfig, le
148149
}
149150

150151
func (dc *RootCommand) Execute(ctx domain.Context) error {
152+
slog.Info("Executing command", "args", ctx.Args(), "executor", ctx.Executor())
151153
name := ctx.Args()[0]
152154

153155
c, ok := dc.cmds[name]

pkg/bot/slack/bot.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import (
44
"context"
55
"fmt"
66
"github.com/kballard/go-shellquote"
7+
"github.com/samber/lo"
78
"github.com/slack-go/slack"
89
"github.com/slack-go/slack/slackevents"
910
"github.com/slack-go/slack/socketmode"
1011
"github.com/traPtitech/DevOpsBot/pkg/config"
1112
"github.com/traPtitech/DevOpsBot/pkg/domain"
1213
"go.uber.org/zap"
14+
"log/slog"
15+
"regexp"
1316
"strings"
1417
)
1518

@@ -94,17 +97,55 @@ func (s *slackBot) handle(e socketmode.Event) error {
9497
return nil
9598
}
9699

100+
var mentionRegexp = regexp.MustCompile("^<@(\\w+)(?:\\|\\w+)?>")
101+
102+
func (s *slackBot) getExecutorID(ev *slackevents.MessageEvent) (executorID string, commandText string, ok bool) {
103+
executorID = ev.User
104+
commandText = ev.Text
105+
106+
if ev.BotID == "" {
107+
// Normal execution by user
108+
return executorID, commandText, true
109+
}
110+
111+
// Execution by bots - check if they are the trusted workflow members
112+
executorID = ev.BotID
113+
mentionIndices := mentionRegexp.FindStringSubmatchIndex(commandText)
114+
if !lo.Contains(config.C.Slack.TrustedWorkflows, executorID) {
115+
// If they are not trusted, ignore bots
116+
if mentionIndices != nil {
117+
// Log bot ID as they are difficult to get from UI
118+
slog.Info("Skipping impersonation request from bot", "bot_id", executorID, "display_name", ev.Username)
119+
}
120+
return "", "", false
121+
}
122+
123+
// Check if the workflow is impersonating execution user
124+
if mentionIndices != nil && mentionIndices[0] == 0 {
125+
// Impersonate user
126+
executorID = commandText[mentionIndices[2]:mentionIndices[3]]
127+
// Trim the mention part
128+
commandText = commandText[mentionIndices[1]:]
129+
commandText = strings.TrimSpace(commandText)
130+
return executorID, commandText, true
131+
} else {
132+
// If they are not impersonating, fallback the executor to its own ID
133+
return executorID, commandText, true
134+
}
135+
}
136+
97137
func (s *slackBot) handleEventsAPI(e *slackevents.EventsAPIEvent) error {
98138
switch ev := e.InnerEvent.Data.(type) {
99139
case *slackevents.MessageEvent:
100140
// Validate command execution context
101-
if ev.BotID != "" {
102-
return nil // Ignore bots
141+
executorID, commandText, ok := s.getExecutorID(ev)
142+
if !ok {
143+
return nil // Not a valid user
103144
}
104145
if ev.Channel != config.C.Slack.ChannelID {
105146
return nil // Ignore messages not from the specified channel
106147
}
107-
if !strings.HasPrefix(ev.Text, config.C.Prefix) {
148+
if !strings.HasPrefix(commandText, config.C.Prefix) {
108149
return nil // Command prefix does not match
109150
}
110151

@@ -113,8 +154,8 @@ func (s *slackBot) handleEventsAPI(e *slackevents.EventsAPIEvent) error {
113154
Channel: ev.Channel,
114155
Timestamp: ev.TimeStamp,
115156
}
116-
commandText := strings.Trim(ev.Text, config.C.Prefix)
117-
return s.executeCommand(commandText, messageRef, ev.User)
157+
commandText = strings.Trim(commandText, config.C.Prefix)
158+
return s.executeCommand(commandText, messageRef, executorID)
118159
default:
119160
return nil
120161
}

pkg/config/config.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ type SlackConfig struct {
4848
AppToken string `mapstructure:"appToken" yaml:"appToken"`
4949
// ChannelID is the channel in which to await for commands
5050
ChannelID string `mapstructure:"channelID" yaml:"channelID"`
51+
// TrustedWorkflows is the list of bot IDs of trusted workflows.
52+
//
53+
// Trusted workflows are allowed to impersonate the execution user via adding user mention at the start of message.
54+
TrustedWorkflows []string `mapstructure:"trustedWorkflows" yaml:"trustedWorkflows"`
5155
}
5256

5357
type Stamps struct {
@@ -85,7 +89,8 @@ type CommandConfig struct {
8589
ArgsSyntax string `mapstructure:"argsSyntax" yaml:"argsSyntax"`
8690
// ArgsPrefix is always prefixed the arguments (before the user-provided arguments, if any) when executing the command template.
8791
ArgsPrefix []string `mapstructure:"argsPrefix" yaml:"argsPrefix"`
88-
// Operators is an optional list of traQ user IDs who are allowed to execute this command (and any sub-commands).
92+
// Operators is an optional list of user IDs (traQ IDs in traQ, member or bot IDs in Slack)
93+
// who are allowed to execute this command (and any sub-commands).
8994
// If left empty, everyone will be able to execute this command (and any sub-commands).
9095
Operators []string `mapstructure:"operators" yaml:"operators"`
9196

@@ -116,6 +121,7 @@ func init() {
116121
viper.SetDefault("slack.oauthToken", "")
117122
viper.SetDefault("slack.appToken", "")
118123
viper.SetDefault("slack.channelID", "")
124+
viper.SetDefault("slack.trustedWorkflows", nil)
119125

120126
viper.SetDefault("prefix", "/")
121127

0 commit comments

Comments
 (0)