-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Datadog Incident Management plugin support (#46271)
* Implement datadog plugin * Add unit tests * Add fallback recipient config * Rename to Datadog Incident Management * Update tests * Datadog Incident Management * Update tctl resource plugin command * Typos * Lint * Exclude api changes for now * Set channel size * Address feedback - Add PluginShutdownTimeout const - Support api endpoint configuration - Add additional godocs/comments * Comment about datadog client package * Document Datadog API types * Only post resolution message when the AR is resolved * Fix lint * Unused function --------- Co-authored-by: hugoShaka <hugo.hervieux@goteleport.com>
- Loading branch information
1 parent
c86f46d
commit 9a0f8d6
Showing
23 changed files
with
1,857 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/* | ||
* Teleport | ||
* Copyright (C) 2024 Gravitational, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package common | ||
|
||
import "time" | ||
|
||
const ( | ||
// PluginShutdownTimeout defines the timeout for plugins to gracefully shutdown. | ||
PluginShutdownTimeout = 15 * time.Second | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
ACCESS_PLUGIN = datadog | ||
|
||
include ../common.mk |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# Teleport Datadog Incident Management plugin | ||
|
||
The Teleport Access API provides a simple Datadog Incident Management plugin that | ||
creates incidents in Datadog when an access request is created. You can find the | ||
Teleport Access API in the main Teleport repository and the Datadog Incident | ||
Management plugin in `https://github.com/gravitational/teleport/tree/master/integrations/access/datadog`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* Teleport | ||
* Copyright (C) 2024 Gravitational, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package datadog | ||
|
||
import ( | ||
"github.com/gravitational/teleport/integrations/access/common" | ||
) | ||
|
||
const ( | ||
// datadogPluginName is used to tag Datadog GenericPluginData and as a Delegator in Audit log. | ||
datadogPluginName = "datadog" | ||
) | ||
|
||
// NewDatadogApp initializes a new teleport-datadog app and returns it. | ||
func NewDatadogApp(conf *Config) *common.BaseApp { | ||
return common.NewApp(conf, datadogPluginName) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
/* | ||
* Teleport | ||
* Copyright (C) 2024 Gravitational, Inc. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU Affero General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU Affero General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU Affero General Public License | ||
* along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package datadog | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/url" | ||
"strings" | ||
"text/template" | ||
|
||
"github.com/gravitational/trace" | ||
|
||
"github.com/gravitational/teleport/api/types" | ||
"github.com/gravitational/teleport/api/types/accesslist" | ||
"github.com/gravitational/teleport/integrations/access/accessrequest" | ||
"github.com/gravitational/teleport/integrations/access/common" | ||
"github.com/gravitational/teleport/integrations/lib" | ||
pd "github.com/gravitational/teleport/integrations/lib/plugindata" | ||
) | ||
|
||
// Bot is a Datadog client that works with AccessRequest. | ||
// It is responsible for creating/updating Datadog incidents when access request | ||
// events occur. | ||
type Bot struct { | ||
datadog *Datadog | ||
clusterName string | ||
webProxyURL *url.URL | ||
} | ||
|
||
var incidentSummaryTemplate = template.Must(template.New("incident summary").Parse( | ||
`You have a new Access Request: | ||
ID: {{.ID}} | ||
Cluster: {{.ClusterName}} | ||
User: {{.User}} | ||
Role(s): {{range $index, $element := .Roles}}{{if $index}}, {{end}}{{ . }}{{end}} | ||
{{if .RequestLink}}Link: {{.RequestLink}}{{end}} `, | ||
)) | ||
var reviewNoteTemplate = template.Must(template.New("review note").Parse( | ||
`{{.Author}} reviewed the request. | ||
Resolution: {{.ProposedState}}. | ||
{{if .Reason}}Reason: {{.Reason}}.{{end}}`, | ||
)) | ||
var resolutionNoteTemplate = template.Must(template.New("resolution note").Parse( | ||
`Access request is {{.Resolution}} | ||
{{if .ResolveReason}}Reason: {{.ResolveReason}}{{end}}`, | ||
)) | ||
|
||
// SupportedApps are the apps supported by this bot. | ||
func (b Bot) SupportedApps() []common.App { | ||
return []common.App{ | ||
accessrequest.NewApp(b), | ||
} | ||
} | ||
|
||
// CheckHealth checks if Datadog connection is healthy. | ||
func (b Bot) CheckHealth(ctx context.Context) error { | ||
return trace.Wrap(b.datadog.CheckHealth(ctx)) | ||
} | ||
|
||
// SendReviewReminders will send a review reminder that an access list needs to be reviewed. | ||
func (b Bot) SendReviewReminders(ctx context.Context, recipient common.Recipient, accessLists []*accesslist.AccessList) error { | ||
return trace.NotImplemented("access list review reminder is not implemented for plugin") | ||
} | ||
|
||
// BroadcastAccessRequestMessage creates an incident for the provided recipients. | ||
func (b Bot) BroadcastAccessRequestMessage(ctx context.Context, recipients []common.Recipient, reqID string, reqData pd.AccessRequestData) (accessrequest.SentMessages, error) { | ||
summary, err := buildIncidentSummary(b.clusterName, reqID, reqData, b.webProxyURL) | ||
if err != nil { | ||
return nil, trace.Wrap(err) | ||
} | ||
incidentData, err := b.datadog.CreateIncident(ctx, summary, recipients, reqData) | ||
if err != nil { | ||
return nil, trace.Wrap(err) | ||
} | ||
var data accessrequest.SentMessages | ||
data = append(data, accessrequest.MessageData{ChannelID: incidentData.ID, MessageID: incidentData.ID}) | ||
return data, nil | ||
} | ||
|
||
// PostReviewReply posts an incident note. | ||
func (b Bot) PostReviewReply(ctx context.Context, channelID, _ string, review types.AccessReview) error { | ||
note, err := buildReviewNoteBody(review) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
return trace.Wrap(b.datadog.PostReviewNote(ctx, channelID, note)) | ||
} | ||
|
||
// NotifyUser will send users a direct notice with the access request status. | ||
func (b Bot) NotifyUser(ctx context.Context, reqID string, reqData pd.AccessRequestData) error { | ||
return trace.NotImplemented("notify user is not implemented for plugin") | ||
} | ||
|
||
// UpdateMessages updates the indicent. | ||
func (b Bot) UpdateMessages(ctx context.Context, reqID string, reqData pd.AccessRequestData, incidents accessrequest.SentMessages, reviews []types.AccessReview) error { | ||
var errors []error | ||
|
||
switch reqData.ResolutionTag { | ||
case pd.ResolvedApproved, pd.ResolvedDenied, pd.ResolvedExpired: | ||
default: | ||
// If the incident is not resolved, we don't need to post any resolution message | ||
// Nor to change its state. Un-resolving an access request should be impossible. | ||
// We can return immediately, nothing to update in the incident. | ||
return nil | ||
} | ||
|
||
note, err := buildResolutionNoteBody(reqData) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
for _, incident := range incidents { | ||
if err := b.datadog.PostReviewNote(ctx, incident.ChannelID, note); err != nil { | ||
errors = append(errors, trace.Wrap(err)) | ||
continue | ||
} | ||
err := b.datadog.ResolveIncident(ctx, incident.ChannelID, "resolved") | ||
errors = append(errors, trace.Wrap(err)) | ||
} | ||
return trace.NewAggregate(errors...) | ||
} | ||
|
||
// FetchRecipient fetches the recipient for the given name. | ||
func (b Bot) FetchRecipient(ctx context.Context, name string) (*common.Recipient, error) { | ||
var kind string | ||
if lib.IsEmail(name) { | ||
kind = common.RecipientKindEmail | ||
name = fmt.Sprintf("@%s", name) | ||
} else { | ||
kind = common.RecipientKindTeam | ||
} | ||
return &common.Recipient{ | ||
Name: name, | ||
ID: name, | ||
Kind: kind, | ||
}, nil | ||
} | ||
|
||
func buildIncidentSummary(clusterName, reqID string, reqData pd.AccessRequestData, webProxyURL *url.URL) (string, error) { | ||
var requestLink string | ||
if webProxyURL != nil { | ||
reqURL := *webProxyURL | ||
reqURL.Path = lib.BuildURLPath("web", "requests", reqID) | ||
requestLink = reqURL.String() | ||
} | ||
|
||
var builder strings.Builder | ||
err := incidentSummaryTemplate.Execute(&builder, struct { | ||
ID string | ||
ClusterName string | ||
RequestLink string | ||
pd.AccessRequestData | ||
}{ | ||
reqID, | ||
clusterName, | ||
requestLink, | ||
reqData, | ||
}) | ||
if err != nil { | ||
return "", trace.Wrap(err) | ||
} | ||
return builder.String(), nil | ||
} | ||
|
||
func buildReviewNoteBody(review types.AccessReview) (string, error) { | ||
var builder strings.Builder | ||
err := reviewNoteTemplate.Execute(&builder, struct { | ||
Author string | ||
ProposedState string | ||
Reason string | ||
}{ | ||
review.Author, | ||
review.ProposedState.String(), | ||
review.Reason, | ||
}) | ||
if err != nil { | ||
return "", trace.Wrap(err) | ||
} | ||
return builder.String(), nil | ||
} | ||
|
||
func buildResolutionNoteBody(reqData pd.AccessRequestData) (string, error) { | ||
var builder strings.Builder | ||
err := resolutionNoteTemplate.Execute(&builder, struct { | ||
Resolution string | ||
ResolveReason string | ||
}{ | ||
statusText(reqData.ResolutionTag), | ||
reqData.ResolutionReason, | ||
}) | ||
if err != nil { | ||
return "", trace.Wrap(err) | ||
} | ||
return builder.String(), nil | ||
} | ||
|
||
func statusText(tag pd.ResolutionTag) string { | ||
var statusEmoji string | ||
status := string(tag) | ||
switch tag { | ||
case pd.Unresolved: | ||
status = "PENDING" | ||
statusEmoji = "⏳" | ||
case pd.ResolvedApproved: | ||
statusEmoji = "✅" | ||
case pd.ResolvedDenied: | ||
statusEmoji = "❌" | ||
case pd.ResolvedExpired: | ||
statusEmoji = "⌛" | ||
} | ||
return fmt.Sprintf("%s %s", statusEmoji, status) | ||
} |
Oops, something went wrong.