Skip to content
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
2 changes: 1 addition & 1 deletion api/types/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const (
PluginTypePagerDuty = "pagerduty"
// PluginTypeMattermost is the PagerDuty access plugin
PluginTypeMattermost = "mattermost"
// PluginTypeDiscord indicates the Discord plugin
// PluginTypeDiscord indicates the Discord access plugin
PluginTypeDiscord = "discord"
)

Expand Down
54 changes: 42 additions & 12 deletions integrations/access/discord/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/access/common"
"github.com/gravitational/teleport/integrations/lib"
"github.com/gravitational/teleport/integrations/lib/logger"
pd "github.com/gravitational/teleport/integrations/lib/plugindata"
)

const discordMaxConns = 100
const discordHTTPTimeout = 10 * time.Second
const discordRedColor = 13771309 // Green OxD2222D
const discordGreenColor = 2328611 // Red 0x2328611
const discordStatusUpdateTimeout = 10 * time.Second

// DiscordBot is a discord client that works with AccessRequest.
// It's responsible for formatting and posting a message on Discord when an
Expand All @@ -47,22 +49,50 @@ type DiscordBot struct {
webProxyURL *url.URL
}

// onAfterResponseDiscord resty error function for Discord
func onAfterResponseDiscord(_ *resty.Client, resp *resty.Response) error {
if resp.IsSuccess() {
return nil
}
// onAfterResponseDiscord creates and configures a post-response
// handler for Discord Requests. Handles routing status updates
// through to the status sink (if supplied).
func onAfterResponseDiscord(statusSink common.StatusSink) resty.ResponseMiddleware {
return func(_ *resty.Client, resp *resty.Response) error {
if statusSink != nil {
emitStatusUpdate(resp, statusSink)
}

var result DiscordResponse
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return trace.Wrap(err)
}
if resp.IsSuccess() {
return nil
}

var result DiscordResponse
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return trace.Wrap(err)
}

if result.Message != "" {
return trace.Errorf("%s (code: %v, status: %d)", result.Message, result.Code, resp.StatusCode())
}

if result.Message != "" {
return trace.Errorf("%s (code: %v, status: %d)", result.Message, result.Code, resp.StatusCode())
return trace.Errorf("Discord API returned error: %s (status: %d)", string(resp.Body()), resp.StatusCode())
}
}

return trace.Errorf("Discord API returned error: %s (status: %d)", string(resp.Body()), resp.StatusCode())
func emitStatusUpdate(resp *resty.Response, statusSink common.StatusSink) {
status := common.StatusFromStatusCode(resp.StatusCode())

// There is sensible context in scope for us to use when emitting the
// status update. We can't use the context from the Resty response,
// as that could already be canceled, which would block us from emitting
// a status update showing that the plugin is currently broken.
//
// Using the background context with a reasonable timeout seems the
// least-bad option.
ctx, cancel := context.WithTimeout(context.Background(), discordStatusUpdateTimeout)
defer cancel()

if err := statusSink.Emit(ctx, status); err != nil {
logger.Get(resp.Request.Context()).
WithError(err).
Errorf("Error while emitting Discord plugin status: %v", err)
}
}

func (b DiscordBot) CheckHealth(ctx context.Context) error {
Expand Down
36 changes: 27 additions & 9 deletions integrations/access/discord/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package discord

import (
"context"
"net/http"
"net/url"

Expand All @@ -25,6 +26,7 @@ import (

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/access/common"
"github.com/gravitational/teleport/integrations/access/common/teleport"
"github.com/gravitational/teleport/integrations/lib"
)

Expand All @@ -33,6 +35,15 @@ const discordAPIUrl = "https://discord.com/api/"
type Config struct {
common.BaseConfig
Discord common.GenericAPIConfig

// Teleport is a handle to the client to use when communicating with
// the Teleport auth server. The PagerDuty app will create a GRPC-
// based client on startup if this is not set.
Client teleport.Client

// StatusSink receives any status updates from the plugin for
// further processing. Status updates will be ignored if not set.
StatusSink common.StatusSink
}

// CheckAndSetDefaults checks the config struct for any logical errors, and sets default values
Expand All @@ -45,6 +56,9 @@ func (c *Config) CheckAndSetDefaults() error {
if c.Discord.Token == "" {
return trace.BadParameter("missing required value discord.token")
}
if c.Discord.APIURL == "" {
c.Discord.APIURL = discordAPIUrl
}
if c.Log.Output == "" {
c.Log.Output = "stderr"
}
Expand All @@ -61,6 +75,16 @@ func (c *Config) CheckAndSetDefaults() error {
return nil
}

// GetTeleportClient implements PluginConfiguration. If a pre-created client
// was supplied on construction, this method will return that. If not, an RPC
// client will be created using the values in the config.
func (c *Config) GetTeleportClient(ctx context.Context) (teleport.Client, error) {
if c.Client != nil {
return c.Client, nil
}
return c.BaseConfig.GetTeleportClient(ctx)
}

// NewBot initializes the new Discord message generator (DiscordBot)
func (c *Config) NewBot(clusterName, webProxyAddr string) (common.MessagingBot, error) {
var (
Expand All @@ -83,17 +107,11 @@ func (c *Config) NewBot(clusterName, webProxyAddr string) (common.MessagingBot,
MaxIdleConnsPerHost: discordMaxConns,
},
}).
SetBaseURL(c.Discord.APIURL).
SetHeader("Content-Type", "application/json").
SetHeader("Accept", "application/json").
SetHeader("Authorization", token)

// APIURL parameter is set only in tests
if endpoint := c.Discord.APIURL; endpoint != "" {
client.SetBaseURL(endpoint)
} else {
client.SetBaseURL(discordAPIUrl)
client.OnAfterResponse(onAfterResponseDiscord)
}
SetHeader("Authorization", token).
OnAfterResponse(onAfterResponseDiscord(c.StatusSink))

return DiscordBot{
client: client,
Expand Down