Skip to content

Commit

Permalink
Add proto definitions and plugin spec Microsoft Teams no code integra…
Browse files Browse the repository at this point in the history
…tion (#46640)

* Add proto definitions for MsTeams no code integration

* Add access monitoring rule and hosted support to msteams plugin

* Fix linting

* Fix go sum mismatch caused by msteams dependency

* Fix linting in access monitoring rules
  • Loading branch information
EdwardDowling authored Nov 4, 2024
1 parent 176afc1 commit 95b8489
Show file tree
Hide file tree
Showing 20 changed files with 2,717 additions and 2,059 deletions.
17 changes: 17 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6255,6 +6255,8 @@ message PluginSpecV1 {
PluginAWSICSettings aws_ic = 16;
// Settings for the Email Access Request plugin
PluginEmailSettings email = 17;
// Settings for the Microsoft Teams plugin
PluginMSTeamsSettings msteams = 18;
}

// generation contains a unique ID that should:
Expand Down Expand Up @@ -6634,6 +6636,21 @@ message SMTPSpec {
string start_tls_policy = 3;
}

// PluginMSTeamsSettings defines the settings for a Microsoft Teams integration plugin
message PluginMSTeamsSettings {
option (gogoproto.equal) = true;
// AppId is the Microsoft application ID (uuid, for Azure bots must be underlying app id, not bot's id).
string app_id = 1;
// TenantId is the Microsoft tenant ID.
string tenant_id = 2;
// TeamsAppId is the Microsoft teams application ID.
string teams_app_id = 3;
// Region to be used by the Microsoft Graph API client.
string region = 4;
// DefaultRecipient is the default recipient to use if no access monitoring rules are specified.
string default_recipient = 5;
}

message PluginBootstrapCredentialsV1 {
oneof credentials {
PluginOAuth2AuthorizationCodeCredentials oauth2_authorization_code = 1;
Expand Down
4 changes: 4 additions & 0 deletions api/types/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ const (
PluginTypeAWSIdentityCenter = "aws-identity-center"
// PluginTypeEmail indicates an Email Access Request plugin
PluginTypeEmail = "email"
// PluginTypeMSTeams indicates a Microsoft Teams integration
PluginTypeMSTeams = "msteams"
)

// PluginSubkind represents the type of the plugin, e.g., access request, MDM etc.
Expand Down Expand Up @@ -541,6 +543,8 @@ func (p *PluginV1) GetType() PluginType {
return PluginTypeAWSIdentityCenter
case *PluginSpecV1_Email:
return PluginTypeEmail
case *PluginSpecV1_Msteams:
return PluginTypeMSTeams
default:
return PluginTypeUnknown
}
Expand Down
4,417 changes: 2,462 additions & 1,955 deletions api/types/types.pb.go

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion integrations/access/accessmonitoring/access_monitoring_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/gravitational/teleport/integrations/access/common"
"github.com/gravitational/teleport/integrations/access/common/teleport"
"github.com/gravitational/teleport/integrations/lib/logger"
"github.com/gravitational/teleport/integrations/lib/stringset"
)

const (
Expand Down Expand Up @@ -159,7 +160,7 @@ func (amrh *RuleHandler) RecipientsFromAccessMonitoringRules(ctx context.Context
for _, recipient := range rule.Spec.Notification.Recipients {
rec, err := amrh.fetchRecipientCallback(ctx, recipient)
if err != nil {
log.WithError(err).Warn("Failed to fetch plugin recipients based on Access moniotring rule recipients")
log.WithError(err).Warn("Failed to fetch plugin recipients based on Access monitoring rule recipients")
continue
}
recipientSet.Add(*rec)
Expand All @@ -168,6 +169,26 @@ func (amrh *RuleHandler) RecipientsFromAccessMonitoringRules(ctx context.Context
return &recipientSet
}

// RawRecipientsFromAccessMonitoringRules returns the recipients that result from the Access Monitoring Rules being applied to the given Access Request without converting to the rich recipient type.
func (amrh *RuleHandler) RawRecipientsFromAccessMonitoringRules(ctx context.Context, req types.AccessRequest) []string {
log := logger.Get(ctx)
recipientSet := stringset.New()
for _, rule := range amrh.getAccessMonitoringRules() {
match, err := MatchAccessRequest(rule.Spec.Condition, req)
if err != nil {
log.WithError(err).WithField("rule", rule.Metadata.Name).
Warn("Failed to parse access monitoring notification rule")
}
if !match {
continue
}
for _, recipient := range rule.Spec.Notification.Recipients {
recipientSet.Add(recipient)
}
}
return recipientSet.ToSlice()
}

func (amrh *RuleHandler) getAllAccessMonitoringRules(ctx context.Context) ([]*accessmonitoringrulesv1.AccessMonitoringRule, error) {
var resources []*accessmonitoringrulesv1.AccessMonitoringRule
var nextToken string
Expand Down
9 changes: 9 additions & 0 deletions integrations/access/common/recipient.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ func (s *RecipientSet) ToSlice() []Recipient {
return recipientSlice
}

// GetNames returns a slice of the recipient names in the set.
func (s *RecipientSet) GetNames() []string {
names := make([]string, 0, len(s.recipients))
for _, recipient := range s.recipients {
names = append(names, recipient.Name)
}
return names
}

// ForEach applies run the given func with each recipient in the set as the argument.
func (s *RecipientSet) ForEach(f func(r Recipient)) {
for _, v := range s.recipients {
Expand Down
49 changes: 18 additions & 31 deletions integrations/access/msteams/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,11 @@ const (
type App struct {
conf Config

apiClient teleport.Client
bot *Bot
mainJob lib.ServiceJob
watcherJob lib.ServiceJob
pd *pd.CompareAndSwap[PluginData]

apiClient teleport.Client
bot *Bot
mainJob lib.ServiceJob
watcherJob lib.ServiceJob
pd *pd.CompareAndSwap[PluginData]
log *slog.Logger
accessMonitoringRules *accessmonitoring.RuleHandler

Expand Down Expand Up @@ -145,27 +144,14 @@ func (a *App) init(ctx context.Context) error {
webProxyAddr = pong.ProxyPublicAddr
}

a.bot, err = NewBot(a.conf.MSAPI, pong.ClusterName, webProxyAddr, a.log)
a.bot, err = NewBot(&a.conf, pong.ClusterName, webProxyAddr, a.log)
if err != nil {
return trace.Wrap(err)
}

a.accessMonitoringRules = accessmonitoring.NewRuleHandler(accessmonitoring.RuleHandlerConfig{
Client: a.apiClient,
PluginName: pluginName,
// Map msteams.RecipientData onto the common recipient type used
// by the access monitoring rules watcher.
FetchRecipientCallback: func(ctx context.Context, name string) (*common.Recipient, error) {
msTeamsRecipient, err := a.bot.FetchRecipient(ctx, name)
if err != nil {
return nil, trace.Wrap(err)
}
return &common.Recipient{
Name: name,
ID: msTeamsRecipient.ID,
Kind: string(msTeamsRecipient.Kind),
}, nil
},
})

return a.initBot(ctx)
Expand All @@ -185,6 +171,13 @@ func (a *App) initBot(ctx context.Context) error {
"name", teamsApp.DisplayName,
"id", teamsApp.ID)

if err := a.bot.CheckHealth(ctx); err != nil {

a.log.WarnContext(ctx, "MS Teams healthcheck failed",
"name", teamsApp.DisplayName,
"id", teamsApp.ID)
}

if !a.conf.Preload {
return nil
}
Expand Down Expand Up @@ -212,9 +205,6 @@ func (a *App) initBot(ctx context.Context) error {

// run starts the main process
func (a *App) run(ctx context.Context) error {

process := lib.MustGetProcess(ctx)

watchKinds := []types.WatchKind{
{Kind: types.KindAccessRequest},
{Kind: types.KindAccessMonitoringRule},
Expand All @@ -237,6 +227,7 @@ func (a *App) run(ctx context.Context) error {
return trace.Wrap(err)
}

process := lib.MustGetProcess(ctx)
process.SpawnCriticalJob(watcherJob)

ok, err := watcherJob.WaitReady(ctx)
Expand All @@ -254,6 +245,7 @@ func (a *App) run(ctx context.Context) error {
return trace.Wrap(err, "initializing Access Monitoring Rule cache")
}
}

a.watcherJob = watcherJob
a.watcherJob.SetReady(ok)
if ok {
Expand Down Expand Up @@ -530,14 +522,10 @@ func (a *App) getMessageRecipients(ctx context.Context, req types.AccessRequest)
// We receive a set from GetRawRecipientsFor but we still might end up with duplicate channel names.
// This can happen if this set contains the channel `C` and the email for channel `C`.
recipientSet := stringset.New()

a.log.DebugContext(ctx, "Getting suggested reviewer recipients")
accessRuleRecipients := a.accessMonitoringRules.RecipientsFromAccessMonitoringRules(ctx, req)
accessRuleRecipients.ForEach(func(r common.Recipient) {
recipientSet.Add(r.Name)
})
if recipientSet.Len() != 0 {
return recipientSet.ToSlice()
accessRuleRecipients := a.accessMonitoringRules.RawRecipientsFromAccessMonitoringRules(ctx, req)
if len(accessRuleRecipients) != 0 {
return accessRuleRecipients
}

var validEmailsSuggReviewers []string
Expand All @@ -557,6 +545,5 @@ func (a *App) getMessageRecipients(ctx context.Context, req types.AccessRequest)
recipientSet.Add(recipient)
}
}

return recipientSet.ToSlice()
}
34 changes: 30 additions & 4 deletions integrations/access/msteams/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ import (
"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/access/common"
"github.com/gravitational/teleport/integrations/access/msteams/msapi"
"github.com/gravitational/teleport/integrations/lib"
"github.com/gravitational/teleport/integrations/lib/logger"
"github.com/gravitational/teleport/integrations/lib/plugindata"
)

Expand Down Expand Up @@ -80,10 +82,13 @@ type Bot struct {
clusterName string
// log is the logger
log *slog.Logger
// StatusSink receives any status updates from the plugin for
// further processing. Status updates will be ignored if not set.
StatusSink common.StatusSink
}

// NewBot creates new bot struct
func NewBot(c msapi.Config, clusterName, webProxyAddr string, log *slog.Logger) (*Bot, error) {
func NewBot(c *Config, clusterName, webProxyAddr string, log *slog.Logger) (*Bot, error) {
var (
webProxyURL *url.URL
err error
Expand All @@ -97,14 +102,15 @@ func NewBot(c msapi.Config, clusterName, webProxyAddr string, log *slog.Logger)
}

bot := &Bot{
Config: c,
graphClient: msapi.NewGraphClient(c),
botClient: msapi.NewBotFrameworkClient(c),
Config: c.MSAPI,
graphClient: msapi.NewGraphClient(c.MSAPI),
botClient: msapi.NewBotFrameworkClient(c.MSAPI),
recipients: make(map[string]RecipientData),
webProxyURL: webProxyURL,
clusterName: clusterName,
mu: &sync.RWMutex{},
log: log,
StatusSink: c.StatusSink,
}

return bot, nil
Expand Down Expand Up @@ -448,3 +454,23 @@ func (b *Bot) checkChannelURL(recipient string) (*Channel, bool) {

return &channel, true
}

// CheckHealth checks if the bot can connect to its messaging service
func (b *Bot) CheckHealth(ctx context.Context) error {
_, err := b.graphClient.GetTeamsApp(ctx, b.Config.TeamsAppID)
if b.StatusSink != nil {
status := types.PluginStatusCode_RUNNING
message := ""
if err != nil {
status = types.PluginStatusCode_OTHER_ERROR
message = err.Error()
}
if err := b.StatusSink.Emit(ctx, &types.PluginStatusV1{
Code: status,
ErrorMessage: message,
}); err != nil {
logger.Get(ctx).Errorf("Error while emitting ms teams plugin status: %v", err)
}
}
return trace.Wrap(err)
}
18 changes: 10 additions & 8 deletions integrations/access/msteams/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,20 @@ import (
"github.com/gravitational/teleport/integrations/access/common/teleport"
"github.com/gravitational/teleport/integrations/access/msteams/msapi"
"github.com/gravitational/teleport/integrations/lib"
"github.com/gravitational/teleport/integrations/lib/logger"
)

// Config represents plugin configuration
type Config struct {
// Client is the Teleport API client.
Client teleport.Client
Teleport lib.TeleportConfig
Recipients common.RawRecipientsMap `toml:"role_to_recipients"`
Log logger.Config
MSAPI msapi.Config `toml:"msapi"`
Preload bool `toml:"preload"`
common.BaseConfig
Client teleport.Client
// MSAPI represents MS Graph API and Bot API config.
MSAPI msapi.Config `toml:"msapi"`
// Preload if set to true will preload the potential msteams recipients.
Preload bool `toml:"preload"`

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

// LoadConfig reads the config file, initializes a new Config struct object, and returns it.
Expand Down
Loading

0 comments on commit 95b8489

Please sign in to comment.