Skip to content

Commit

Permalink
feat(slack): Allow trusted workflows to execute (as impersonation)
Browse files Browse the repository at this point in the history
  • Loading branch information
motoki317 committed Jul 5, 2024
1 parent 09a9e93 commit 5c64269
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 6 deletions.
2 changes: 2 additions & 0 deletions pkg/bot/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package bot
import (
"bytes"
"fmt"
"log/slog"
"os"
"os/exec"
"slices"
Expand Down Expand Up @@ -148,6 +149,7 @@ func compileCommands(templates map[string]string, cc []*config.CommandConfig, le
}

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

c, ok := dc.cmds[name]
Expand Down
51 changes: 46 additions & 5 deletions pkg/bot/slack/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"context"
"fmt"
"github.com/kballard/go-shellquote"
"github.com/samber/lo"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode"
"github.com/traPtitech/DevOpsBot/pkg/config"
"github.com/traPtitech/DevOpsBot/pkg/domain"
"go.uber.org/zap"
"log/slog"
"regexp"
"strings"
)

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

var mentionRegexp = regexp.MustCompile("^<@(\\w+)(?:\\|\\w+)?>")

func (s *slackBot) getExecutorID(ev *slackevents.MessageEvent) (executorID string, commandText string, ok bool) {
executorID = ev.User
commandText = ev.Text

if ev.BotID == "" {
// Normal execution by user
return executorID, commandText, true
}

// Execution by bots - check if they are the trusted workflow members
executorID = ev.BotID
mentionIndices := mentionRegexp.FindStringSubmatchIndex(commandText)
if !lo.Contains(config.C.Slack.TrustedWorkflows, executorID) {
// If they are not trusted, ignore bots
if mentionIndices != nil {
// Log bot ID as they are difficult to get from UI
slog.Info("Skipping impersonation request from bot", "bot_id", executorID, "display_name", ev.Username)
}
return "", "", false
}

// Check if the workflow is impersonating execution user
if mentionIndices != nil && mentionIndices[0] == 0 {
// Impersonate user
executorID = commandText[mentionIndices[2]:mentionIndices[3]]
// Trim the mention part
commandText = commandText[mentionIndices[1]:]
commandText = strings.TrimSpace(commandText)
return executorID, commandText, true
} else {
// If they are not impersonating, fallback the executor to its own ID
return executorID, commandText, true
}
}

func (s *slackBot) handleEventsAPI(e *slackevents.EventsAPIEvent) error {
switch ev := e.InnerEvent.Data.(type) {
case *slackevents.MessageEvent:
// Validate command execution context
if ev.BotID != "" {
return nil // Ignore bots
executorID, commandText, ok := s.getExecutorID(ev)
if !ok {
return nil // Not a valid user
}
if ev.Channel != config.C.Slack.ChannelID {
return nil // Ignore messages not from the specified channel
}
if !strings.HasPrefix(ev.Text, config.C.Prefix) {
if !strings.HasPrefix(commandText, config.C.Prefix) {
return nil // Command prefix does not match
}

Expand All @@ -113,8 +154,8 @@ func (s *slackBot) handleEventsAPI(e *slackevents.EventsAPIEvent) error {
Channel: ev.Channel,
Timestamp: ev.TimeStamp,
}
commandText := strings.Trim(ev.Text, config.C.Prefix)
return s.executeCommand(commandText, messageRef, ev.User)
commandText = strings.Trim(commandText, config.C.Prefix)
return s.executeCommand(commandText, messageRef, executorID)
default:
return nil
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type SlackConfig struct {
AppToken string `mapstructure:"appToken" yaml:"appToken"`
// ChannelID is the channel in which to await for commands
ChannelID string `mapstructure:"channelID" yaml:"channelID"`
// TrustedWorkflows is the list of bot IDs of trusted workflows.
//
// Trusted workflows are allowed to impersonate the execution user via adding user mention at the start of message.
TrustedWorkflows []string `mapstructure:"trustedWorkflows" yaml:"trustedWorkflows"`
}

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

Expand Down Expand Up @@ -116,6 +121,7 @@ func init() {
viper.SetDefault("slack.oauthToken", "")
viper.SetDefault("slack.appToken", "")
viper.SetDefault("slack.channelID", "")
viper.SetDefault("slack.trustedWorkflows", nil)

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

Expand Down

0 comments on commit 5c64269

Please sign in to comment.