Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide private channel names in show config command #1467

Merged
merged 5 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions pkg/bot/slack_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
pb "github.com/kubeshop/botkube/pkg/api/cloudslack"
"github.com/kubeshop/botkube/pkg/bot/interactive"
"github.com/kubeshop/botkube/pkg/config"
conversationx "github.com/kubeshop/botkube/pkg/conversation"
"github.com/kubeshop/botkube/pkg/execute"
"github.com/kubeshop/botkube/pkg/execute/command"
"github.com/kubeshop/botkube/pkg/formatx"
Expand Down Expand Up @@ -87,7 +88,7 @@ func NewCloudSlack(log logrus.FieldLogger,
return nil, err
}

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

options := []slack.MsgOption{
b.renderer.RenderInteractiveMessage(resp),
Expand Down Expand Up @@ -766,3 +767,22 @@ func (b *CloudSlack) GetStatus() health.PlatformStatus {
Reason: b.failureReason,
}
}

func cloudSlackChannelsConfigFrom(log logrus.FieldLogger, channelsCfg config.IdentifiableMap[config.CloudSlackChannel]) map[string]channelConfigByName {
channels := make(map[string]channelConfigByName)
for channAlias, channCfg := range channelsCfg {
normalizedChannelName, changed := conversationx.NormalizeChannelIdentifier(channCfg.Name)
if changed {
log.Warnf("Channel name %q has been normalized to %q", channCfg.Name, normalizedChannelName)
}
channCfg.Name = normalizedChannelName

channels[channCfg.Identifier()] = channelConfigByName{
ChannelBindingsByName: channCfg.ChannelBindingsByName,
alias: channAlias,
notify: !channCfg.Notification.Disabled,
}
}

return channels
}
42 changes: 34 additions & 8 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
koanfyaml "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/rawbytes"
"github.com/mitchellh/mapstructure"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
Expand Down Expand Up @@ -97,7 +98,7 @@ const (
// DiscordCommPlatformIntegration defines Discord integration.
DiscordCommPlatformIntegration CommPlatformIntegration = "discord"

//ElasticsearchCommPlatformIntegration defines Elasticsearch integration.
// ElasticsearchCommPlatformIntegration defines Elasticsearch integration.
ElasticsearchCommPlatformIntegration CommPlatformIntegration = "elasticsearch"

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

// CloudSlackChannel contains configuration bindings per channel.
type CloudSlackChannel struct {
ChannelBindingsByName `yaml:",inline" mapstructure:",squash"`

// ChannelID is the Slack ID of the channel.
// Currently, it is used for AI plugin as it has ability to fetch the Botkube Agent configuration.
// Later it can be used for deep linking to a given channel, see: https://api.slack.com/reference/deep-linking#app_channel
ChannelID string `yaml:"channelID"`
// Alias is an optional public alias for a private channel.
Alias *string `yaml:"alias,omitempty"`
}

// ChannelBindingsByName contains configuration bindings per channel.
type ChannelBindingsByName struct {
Name string `yaml:"name"`
Expand Down Expand Up @@ -498,12 +511,12 @@ type SocketSlack struct {

// CloudSlack configuration for multi-slack support
type CloudSlack struct {
Enabled bool `yaml:"enabled"`
Channels IdentifiableMap[ChannelBindingsByName] `yaml:"channels" validate:"required_if=Enabled true,dive,omitempty,min=1"`
Token string `yaml:"token"`
BotID string `yaml:"botID,omitempty"`
Server GRPCServer `yaml:"server"`
ExecutionEventStreamingDisabled bool `yaml:"executionEventStreamingDisabled"`
Enabled bool `yaml:"enabled"`
Channels IdentifiableMap[CloudSlackChannel] `yaml:"channels" validate:"required_if=Enabled true,dive,omitempty,min=1"`
Token string `yaml:"token"`
BotID string `yaml:"botID,omitempty"`
Server GRPCServer `yaml:"server"`
ExecutionEventStreamingDisabled bool `yaml:"executionEventStreamingDisabled"`
}

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

var cfg Config
err = k.Unmarshal("", &cfg)
err = k.UnmarshalWithConf("", &cfg, koanf.UnmarshalConf{
DecoderConfig: &mapstructure.DecoderConfig{
Squash: true, // needed to properly unmarshal CloudSlack channel's ChannelBindingsByName

// also use defaults from koanf.UnmarshalWithConf
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
mapstructure.TextUnmarshallerHookFunc()),
Metadata: nil,
Result: &cfg,
WeaklyTypedInput: true,
},
})
if err != nil {
return nil, LoadWithDefaultsDetails{}, err
}
Expand Down
44 changes: 44 additions & 0 deletions pkg/config/redacted.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package config

import (
"fmt"
)

const redactedSecretStr = "*** REDACTED ***"

// HideSensitiveInfo removes sensitive information from the config.
func HideSensitiveInfo(in Config) Config {
out := in
// TODO: avoid printing sensitive data without need to resetting them manually (which is an error-prone approach)
for key, val := range out.Communications {
val.SocketSlack.AppToken = redactedSecretStr
val.SocketSlack.BotToken = redactedSecretStr
val.Elasticsearch.Password = redactedSecretStr
val.Discord.Token = redactedSecretStr
val.Mattermost.Token = redactedSecretStr
val.CloudSlack.Token = redactedSecretStr
// To keep the printed config readable, we don't print the certificate bytes.
val.CloudSlack.Server.TLS.CACertificate = nil
val.CloudTeams.Server.TLS.CACertificate = nil

// Replace private channel names with aliases
cloudSlackChannels := make(IdentifiableMap[CloudSlackChannel])
for _, channel := range val.CloudSlack.Channels {
if channel.Alias == nil {
cloudSlackChannels[channel.ChannelBindingsByName.Name] = channel
continue
}

outChannel := channel
outChannel.ChannelBindingsByName.Name = fmt.Sprintf("%s (public alias)", *channel.Alias)
outChannel.Alias = nil
cloudSlackChannels[*channel.Alias] = outChannel
}
val.CloudSlack.Channels = cloudSlackChannels

// maps are not addressable: https://stackoverflow.com/questions/42605337/cannot-assign-to-struct-field-in-a-map
out.Communications[key] = val
}

return out
}
34 changes: 4 additions & 30 deletions pkg/execute/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,40 +48,14 @@ func (e *ConfigExecutor) Commands() map[command.Verb]CommandFn {

// Show returns Config in yaml format
func (e *ConfigExecutor) Show(_ context.Context, cmdCtx CommandContext) (interactive.CoreMessage, error) {
cfg, err := e.renderBotkubeConfiguration()
redactedCfg := config.HideSensitiveInfo(e.cfg)
bytes, err := yaml.Marshal(redactedCfg)
if err != nil {
return interactive.CoreMessage{}, fmt.Errorf("while rendering Botkube configuration: %w", err)
}
return respond(cfg, cmdCtx), nil
}

const redactedSecretStr = "*** REDACTED ***"

func (e *ConfigExecutor) renderBotkubeConfiguration() (string, error) {
cfg := e.cfg

// hide sensitive info
// TODO: avoid printing sensitive data without need to resetting them manually (which is an error-prone approach)
for key, val := range cfg.Communications {
val.SocketSlack.AppToken = redactedSecretStr
val.SocketSlack.BotToken = redactedSecretStr
val.Elasticsearch.Password = redactedSecretStr
val.Discord.Token = redactedSecretStr
val.Mattermost.Token = redactedSecretStr
val.CloudSlack.Token = redactedSecretStr

// To keep the printed config readable, we don't print the certificate bytes.
val.CloudSlack.Server.TLS.CACertificate = nil
val.CloudTeams.Server.TLS.CACertificate = nil

// maps are not addressable: https://stackoverflow.com/questions/42605337/cannot-assign-to-struct-field-in-a-map
cfg.Communications[key] = val
}

b, err := yaml.Marshal(cfg)
if err != nil {
return "", err
return interactive.CoreMessage{}, fmt.Errorf("while rendering Botkube configuration: %w", err)
}

return string(b), nil
return respond(string(bytes), cmdCtx), nil
}
Loading