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

Add proto definitions and plugin spec Microsoft Teams no code integration #46640

Merged
merged 8 commits into from
Nov 4, 2024
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;
EdwardDowling marked this conversation as resolved.
Show resolved Hide resolved
// 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
}

EdwardDowling marked this conversation as resolved.
Show resolved Hide resolved
// 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
EdwardDowling marked this conversation as resolved.
Show resolved Hide resolved
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 {
EdwardDowling marked this conversation as resolved.
Show resolved Hide resolved
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
Loading