Skip to content

Commit 9b6b3ce

Browse files
authored
Hide private channel names in show config command (#1467)
* Extend Cloud Slack channel configuration * Hide private channels in `show config` command
1 parent 179f52d commit 9b6b3ce

File tree

4 files changed

+105
-41
lines changed

4 files changed

+105
-41
lines changed

pkg/bot/slack_cloud.go

+23-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
pb "github.com/kubeshop/botkube/pkg/api/cloudslack"
3030
"github.com/kubeshop/botkube/pkg/bot/interactive"
3131
"github.com/kubeshop/botkube/pkg/config"
32+
conversationx "github.com/kubeshop/botkube/pkg/conversation"
3233
"github.com/kubeshop/botkube/pkg/execute"
3334
"github.com/kubeshop/botkube/pkg/execute/command"
3435
"github.com/kubeshop/botkube/pkg/formatx"
@@ -87,7 +88,7 @@ func NewCloudSlack(log logrus.FieldLogger,
8788
return nil, err
8889
}
8990

90-
channels := slackChannelsConfigFrom(log, cfg.Channels)
91+
channels := cloudSlackChannelsConfigFrom(log, cfg.Channels)
9192
if err != nil {
9293
return nil, fmt.Errorf("while producing channels configuration map by ID: %w", err)
9394
}
@@ -573,15 +574,15 @@ func (b *CloudSlack) send(ctx context.Context, event slackMessage, resp interact
573574
// TODO: Currently, we don't get the channel ID once we use modal. This needs to be investigated and fixed.
574575
//
575576
// we can open modal only if we have a TriggerID (it's available when user clicks a button)
576-
//if resp.Type == api.PopupMessage && event.TriggerID != "" {
577+
// if resp.Type == api.PopupMessage && event.TriggerID != "" {
577578
// modalView := b.renderer.RenderModal(resp)
578579
// modalView.PrivateMetadata = event.Channel
579580
// _, err := b.client.OpenViewContext(ctx, event.TriggerID, modalView)
580581
// if err != nil {
581582
// return fmt.Errorf("while opening modal: %w", err)
582583
// }
583584
// return nil
584-
//}
585+
// }
585586

586587
options := []slack.MsgOption{
587588
b.renderer.RenderInteractiveMessage(resp),
@@ -766,3 +767,22 @@ func (b *CloudSlack) GetStatus() health.PlatformStatus {
766767
Reason: b.failureReason,
767768
}
768769
}
770+
771+
func cloudSlackChannelsConfigFrom(log logrus.FieldLogger, channelsCfg config.IdentifiableMap[config.CloudSlackChannel]) map[string]channelConfigByName {
772+
channels := make(map[string]channelConfigByName)
773+
for channAlias, channCfg := range channelsCfg {
774+
normalizedChannelName, changed := conversationx.NormalizeChannelIdentifier(channCfg.Name)
775+
if changed {
776+
log.Warnf("Channel name %q has been normalized to %q", channCfg.Name, normalizedChannelName)
777+
}
778+
channCfg.Name = normalizedChannelName
779+
780+
channels[channCfg.Identifier()] = channelConfigByName{
781+
ChannelBindingsByName: channCfg.ChannelBindingsByName,
782+
alias: channAlias,
783+
notify: !channCfg.Notification.Disabled,
784+
}
785+
}
786+
787+
return channels
788+
}

pkg/config/config.go

+34-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
koanfyaml "github.com/knadh/koanf/parsers/yaml"
1212
"github.com/knadh/koanf/providers/env"
1313
"github.com/knadh/koanf/providers/rawbytes"
14+
"github.com/mitchellh/mapstructure"
1415
"golang.org/x/text/cases"
1516
"golang.org/x/text/language"
1617
)
@@ -97,7 +98,7 @@ const (
9798
// DiscordCommPlatformIntegration defines Discord integration.
9899
DiscordCommPlatformIntegration CommPlatformIntegration = "discord"
99100

100-
//ElasticsearchCommPlatformIntegration defines Elasticsearch integration.
101+
// ElasticsearchCommPlatformIntegration defines Elasticsearch integration.
101102
ElasticsearchCommPlatformIntegration CommPlatformIntegration = "elasticsearch"
102103

103104
// WebhookCommPlatformIntegration defines an outgoing webhook integration.
@@ -195,6 +196,18 @@ type IncomingWebhook struct {
195196
InClusterBaseURL string `yaml:"inClusterBaseURL"`
196197
}
197198

199+
// CloudSlackChannel contains configuration bindings per channel.
200+
type CloudSlackChannel struct {
201+
ChannelBindingsByName `yaml:",inline" mapstructure:",squash"`
202+
203+
// ChannelID is the Slack ID of the channel.
204+
// Currently, it is used for AI plugin as it has ability to fetch the Botkube Agent configuration.
205+
// Later it can be used for deep linking to a given channel, see: https://api.slack.com/reference/deep-linking#app_channel
206+
ChannelID string `yaml:"channelID"`
207+
// Alias is an optional public alias for a private channel.
208+
Alias *string `yaml:"alias,omitempty"`
209+
}
210+
198211
// ChannelBindingsByName contains configuration bindings per channel.
199212
type ChannelBindingsByName struct {
200213
Name string `yaml:"name"`
@@ -498,12 +511,12 @@ type SocketSlack struct {
498511

499512
// CloudSlack configuration for multi-slack support
500513
type CloudSlack struct {
501-
Enabled bool `yaml:"enabled"`
502-
Channels IdentifiableMap[ChannelBindingsByName] `yaml:"channels" validate:"required_if=Enabled true,dive,omitempty,min=1"`
503-
Token string `yaml:"token"`
504-
BotID string `yaml:"botID,omitempty"`
505-
Server GRPCServer `yaml:"server"`
506-
ExecutionEventStreamingDisabled bool `yaml:"executionEventStreamingDisabled"`
514+
Enabled bool `yaml:"enabled"`
515+
Channels IdentifiableMap[CloudSlackChannel] `yaml:"channels" validate:"required_if=Enabled true,dive,omitempty,min=1"`
516+
Token string `yaml:"token"`
517+
BotID string `yaml:"botID,omitempty"`
518+
Server GRPCServer `yaml:"server"`
519+
ExecutionEventStreamingDisabled bool `yaml:"executionEventStreamingDisabled"`
507520
}
508521

509522
// GRPCServer config for gRPC server
@@ -715,7 +728,20 @@ func LoadWithDefaults(configs [][]byte) (*Config, LoadWithDefaultsDetails, error
715728
}
716729

717730
var cfg Config
718-
err = k.Unmarshal("", &cfg)
731+
err = k.UnmarshalWithConf("", &cfg, koanf.UnmarshalConf{
732+
DecoderConfig: &mapstructure.DecoderConfig{
733+
Squash: true, // needed to properly unmarshal CloudSlack channel's ChannelBindingsByName
734+
735+
// also use defaults from koanf.UnmarshalWithConf
736+
DecodeHook: mapstructure.ComposeDecodeHookFunc(
737+
mapstructure.StringToTimeDurationHookFunc(),
738+
mapstructure.StringToSliceHookFunc(","),
739+
mapstructure.TextUnmarshallerHookFunc()),
740+
Metadata: nil,
741+
Result: &cfg,
742+
WeaklyTypedInput: true,
743+
},
744+
})
719745
if err != nil {
720746
return nil, LoadWithDefaultsDetails{}, err
721747
}

pkg/config/redacted.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
const redactedSecretStr = "*** REDACTED ***"
8+
9+
// HideSensitiveInfo removes sensitive information from the config.
10+
func HideSensitiveInfo(in Config) Config {
11+
out := in
12+
// TODO: avoid printing sensitive data without need to resetting them manually (which is an error-prone approach)
13+
for key, val := range out.Communications {
14+
val.SocketSlack.AppToken = redactedSecretStr
15+
val.SocketSlack.BotToken = redactedSecretStr
16+
val.Elasticsearch.Password = redactedSecretStr
17+
val.Discord.Token = redactedSecretStr
18+
val.Mattermost.Token = redactedSecretStr
19+
val.CloudSlack.Token = redactedSecretStr
20+
// To keep the printed config readable, we don't print the certificate bytes.
21+
val.CloudSlack.Server.TLS.CACertificate = nil
22+
val.CloudTeams.Server.TLS.CACertificate = nil
23+
24+
// Replace private channel names with aliases
25+
cloudSlackChannels := make(IdentifiableMap[CloudSlackChannel])
26+
for _, channel := range val.CloudSlack.Channels {
27+
if channel.Alias == nil {
28+
cloudSlackChannels[channel.ChannelBindingsByName.Name] = channel
29+
continue
30+
}
31+
32+
outChannel := channel
33+
outChannel.ChannelBindingsByName.Name = fmt.Sprintf("%s (public alias)", *channel.Alias)
34+
outChannel.Alias = nil
35+
cloudSlackChannels[*channel.Alias] = outChannel
36+
}
37+
val.CloudSlack.Channels = cloudSlackChannels
38+
39+
// maps are not addressable: https://stackoverflow.com/questions/42605337/cannot-assign-to-struct-field-in-a-map
40+
out.Communications[key] = val
41+
}
42+
43+
return out
44+
}

pkg/execute/config.go

+4-30
Original file line numberDiff line numberDiff line change
@@ -48,40 +48,14 @@ func (e *ConfigExecutor) Commands() map[command.Verb]CommandFn {
4848

4949
// Show returns Config in yaml format
5050
func (e *ConfigExecutor) Show(_ context.Context, cmdCtx CommandContext) (interactive.CoreMessage, error) {
51-
cfg, err := e.renderBotkubeConfiguration()
51+
redactedCfg := config.HideSensitiveInfo(e.cfg)
52+
bytes, err := yaml.Marshal(redactedCfg)
5253
if err != nil {
5354
return interactive.CoreMessage{}, fmt.Errorf("while rendering Botkube configuration: %w", err)
5455
}
55-
return respond(cfg, cmdCtx), nil
56-
}
57-
58-
const redactedSecretStr = "*** REDACTED ***"
59-
60-
func (e *ConfigExecutor) renderBotkubeConfiguration() (string, error) {
61-
cfg := e.cfg
62-
63-
// hide sensitive info
64-
// TODO: avoid printing sensitive data without need to resetting them manually (which is an error-prone approach)
65-
for key, val := range cfg.Communications {
66-
val.SocketSlack.AppToken = redactedSecretStr
67-
val.SocketSlack.BotToken = redactedSecretStr
68-
val.Elasticsearch.Password = redactedSecretStr
69-
val.Discord.Token = redactedSecretStr
70-
val.Mattermost.Token = redactedSecretStr
71-
val.CloudSlack.Token = redactedSecretStr
72-
73-
// To keep the printed config readable, we don't print the certificate bytes.
74-
val.CloudSlack.Server.TLS.CACertificate = nil
75-
val.CloudTeams.Server.TLS.CACertificate = nil
7656

77-
// maps are not addressable: https://stackoverflow.com/questions/42605337/cannot-assign-to-struct-field-in-a-map
78-
cfg.Communications[key] = val
79-
}
80-
81-
b, err := yaml.Marshal(cfg)
8257
if err != nil {
83-
return "", err
58+
return interactive.CoreMessage{}, fmt.Errorf("while rendering Botkube configuration: %w", err)
8459
}
85-
86-
return string(b), nil
60+
return respond(string(bytes), cmdCtx), nil
8761
}

0 commit comments

Comments
 (0)