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
7 changes: 5 additions & 2 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5168,14 +5168,17 @@ message PluginSlackAccessSettings {
message PluginOpsgenieAccessSettings {
option (gogoproto.equal) = true;

// Addr is the address of Opsgenie API.
string addr = 1;
reserved 1;
reserved "addr";

// Priority to create Opsgenie alerts with
string priority = 2;
// List of tags to be added to alerts created in Opsgenie
repeated string alert_tags = 3;
// Default on-call schedules to check if none are provided in the access request annotations
repeated string default_schedules = 4;
// APIEndpoint is the address of Opsgenie API.
string api_endpoint = 5;
}

// Defines settings for the OpenAI plugin. Currently there are no settings.
Expand Down
3 changes: 3 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,9 @@ const (
// discovered databases.
DatabaseAdminLabel = TeleportNamespace + "/db-admin"

// ReqAnnotationSchedulesLabel is the request annotation key at which schedules are stored for access plugins.
ReqAnnotationSchedulesLabel = "/schedules"

// CloudAWS identifies that a resource was discovered in AWS.
CloudAWS = "AWS"
// CloudAzure identifies that a resource was discovered in Azure.
Expand Down
25 changes: 25 additions & 0 deletions api/types/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const (
PluginTypeOkta = "okta"
// PluginTypeJamf is the Jamf MDM plugin
PluginTypeJamf = "jamf"
// PluginTypeOpsgenie is the Opsgenie access request plugin
PluginTypeOpsgenie = "opsgenie"
)

// PluginSubkind represents the type of the plugin, e.g., access request, MDM etc.
Expand Down Expand Up @@ -129,6 +131,21 @@ func (p *PluginV1) CheckAndSetDefaults() error {
if bearer.Token == "" {
return trace.BadParameter("Token must be specified")
}
case *PluginSpecV1_Opsgenie:
if settings.Opsgenie == nil {
return trace.BadParameter("settings must be set")
}
if err := settings.Opsgenie.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}

bearer := p.Credentials.GetBearerToken()
if bearer == nil {
return trace.BadParameter("opsgenie plugin must be used with the bearer token credential type")
}
if bearer.Token == "" {
return trace.BadParameter("Token must be specified")
}
case *PluginSpecV1_Jamf:
if settings.Jamf.JamfSpec.ApiEndpoint == "" {
return trace.BadParameter("api endpoint must be set")
Expand Down Expand Up @@ -317,6 +334,14 @@ func (s *PluginOktaSettings) CheckAndSetDefaults() error {
return nil
}

// CheckAndSetDefaults validates and set the default values
func (s *PluginOpsgenieAccessSettings) CheckAndSetDefaults() error {
if s.ApiEndpoint == "" {
return trace.BadParameter("opsgenie api endpoint url must be set")
}
return nil
}

// CheckAndSetDefaults validates and set the default values
func (c *PluginOAuth2AuthorizationCodeCredentials) CheckAndSetDefaults() error {
if c.AuthorizationCode == "" {
Expand Down
2,788 changes: 1,394 additions & 1,394 deletions api/types/types.pb.go

Large diffs are not rendered by default.

23 changes: 19 additions & 4 deletions integrations/access/common/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,16 @@ func (a *BaseApp) onPendingRequest(ctx context.Context, req types.AccessRequest)

reqID := req.GetName()
reqData := pd.AccessRequestData{
User: req.GetUser(),
Roles: req.GetRoles(),
RequestReason: req.GetRequestReason(),
User: req.GetUser(),
Roles: req.GetRoles(),
RequestReason: req.GetRequestReason(),
ResolveAnnotations: req.GetResolveAnnotations(),
}

_, err := a.pluginData.Create(ctx, reqID, GenericPluginData{AccessRequestData: reqData})
switch {
case err == nil:
// This is a new access-request, we have to broadcast it first
// This is a new access-request, we have to broadcast it first.
if recipients := a.getMessageRecipients(ctx, req); len(recipients) > 0 {
if err := a.broadcastMessages(ctx, recipients, reqID, reqData); err != nil {
return trace.Wrap(err)
Expand Down Expand Up @@ -385,6 +386,20 @@ func (a *BaseApp) getMessageRecipients(ctx context.Context, req types.AccessRequ
// This can happen if this set contains the channel `C` and the email for channel `C`.
recipientSet := NewRecipientSet()

switch a.Conf.GetPluginType() {
case types.PluginTypeOpsgenie:
if recipients, ok := req.GetResolveAnnotations()[types.TeleportNamespace+types.ReqAnnotationSchedulesLabel]; ok {
for _, recipient := range recipients {
rec, err := a.bot.FetchRecipient(ctx, recipient)
if err != nil {
log.Warning(err)
}
recipientSet.Add(*rec)
}
return recipientSet.ToSlice()
}
}

validEmailSuggReviewers := []string{}
for _, reviewer := range req.GetSuggestedReviewers() {
if !lib.IsEmail(reviewer) {
Expand Down
8 changes: 8 additions & 0 deletions integrations/access/common/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
grpcbackoff "google.golang.org/grpc/backoff"

"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/access/common/teleport"
"github.com/gravitational/teleport/integrations/lib"
"github.com/gravitational/teleport/integrations/lib/credentials"
Expand All @@ -35,12 +36,14 @@ type PluginConfiguration interface {
GetTeleportClient(ctx context.Context) (teleport.Client, error)
GetRecipients() RawRecipientsMap
NewBot(clusterName string, webProxyAddr string) (MessagingBot, error)
GetPluginType() types.PluginType
}

type BaseConfig struct {
Teleport lib.TeleportConfig
Recipients RawRecipientsMap `toml:"role_to_recipients"`
Log logger.Config
PluginType types.PluginType
}

func (c BaseConfig) GetRecipients() RawRecipientsMap {
Expand Down Expand Up @@ -79,6 +82,11 @@ func (c BaseConfig) GetTeleportClient(ctx context.Context) (teleport.Client, err
return clt, nil
}

// GetPluginType returns the type of plugin this config is for.
func (c BaseConfig) GetPluginType() types.PluginType {
return c.PluginType
}

// GenericAPIConfig holds common configuration use by a messaging service.
// MessagingBots requiring more custom configuration (MSTeams for example) can
// implement their own APIConfig instead.
Expand Down
5 changes: 5 additions & 0 deletions integrations/access/common/recipient.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import (
"github.com/gravitational/teleport/integrations/lib/stringset"
)

const (
// RecipientKindSchedule shows a recipient is a schedule.
RecipientKindSchedule = "schedule"
)

// RawRecipientsMap is a mapping of roles to recipient(s).
type RawRecipientsMap map[string][]string

Expand Down
31 changes: 31 additions & 0 deletions integrations/access/opsgenie/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
Copyright 2023 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package opsgenie

import (
"github.com/gravitational/teleport/integrations/access/common"
)

const (
// opsgeniePluginName is used to tag Opsgenie GenericPluginData and as a Delegator in Audit log.
opsgeniePluginName = "opsgenie"
)

// NewOpsgenieApp initializes a new teleport-opsgenie app and returns it.
func NewOpsgenieApp(conf *Config) *common.BaseApp {
return common.NewApp(conf, opsgeniePluginName)
}
111 changes: 111 additions & 0 deletions integrations/access/opsgenie/bot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
Copyright 2023 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package opsgenie

import (
"context"
"net/url"
"time"

"github.com/gravitational/trace"

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

// Bot is a opsgenie client that works with AccessRequest.
// It's responsible for formatting and Opsgenie alerts when an
// action occurs with an access request: a new request popped up, or a
// request is processed/updated.
type Bot struct {
client *Client
clusterName string
webProxyURL *url.URL
}

// CheckHealth checks if the bot can connect to its messaging service
func (b *Bot) CheckHealth(ctx context.Context) error {
return trace.Wrap(b.client.CheckHealth(ctx))
}

// Broadcast creates an alert for the provided recipients (schedules)
func (b *Bot) Broadcast(ctx context.Context, recipients []common.Recipient, reqID string, reqData pd.AccessRequestData) (data common.SentMessages, err error) {
schedules := []string{}
for _, recipient := range recipients {
schedules = append(schedules, recipient.Name)
}
if len(recipients) == 0 {
schedules = append(schedules, b.client.DefaultSchedules...)
}
opsgenieReqData := RequestData{
User: reqData.User,
Roles: reqData.Roles,
Created: time.Now(),
RequestReason: reqData.RequestReason,
ReviewsCount: reqData.ReviewsCount,
Resolution: Resolution{
Tag: ResolutionTag(reqData.ResolutionTag),
Reason: reqData.ResolutionReason,
},
ResolveAnnotations: types.Labels{
types.TeleportNamespace + types.ReqAnnotationSchedulesLabel: schedules,
},
}
opsgenieData, err := b.client.CreateAlert(ctx, reqID, opsgenieReqData)
if err != nil {
return nil, trace.Wrap(err)
}
data = common.SentMessages{{
ChannelID: opsgenieData.ServiceID,
MessageID: opsgenieData.AlertID,
}}

return data, nil

}

// PostReviewReply posts an alert note.
func (b *Bot) PostReviewReply(ctx context.Context, _ string, alertID string, review types.AccessReview) error {
return trace.Wrap(b.client.PostReviewNote(ctx, alertID, review))
}

// UpdateMessages add notes to the alert containing updates to status.
// This will also resolve alerts based on the resolution tag.
func (b *Bot) UpdateMessages(ctx context.Context, reqID string, data pd.AccessRequestData, alertData common.SentMessages, reviews []types.AccessReview) error {
var errs []error
for _, alert := range alertData {
resolution := Resolution{
Tag: ResolutionTag(data.ResolutionTag),
Reason: data.ResolutionReason,
}
err := b.client.ResolveAlert(ctx, alert.MessageID, resolution)
if err != nil {
errs = append(errs, err)
}
}
return trace.NewAggregate(errs...)
}

// FetchRecipient returns the recipient for the given raw recipient.
func (b *Bot) FetchRecipient(ctx context.Context, recipient string) (*common.Recipient, error) {
return &common.Recipient{
Name: recipient,
ID: recipient,
Kind: common.RecipientKindSchedule,
}, nil
}
Loading