From 0b1471add6918fa56b6dc44e32729451b935283e Mon Sep 17 00:00:00 2001 From: Edward Dowling Date: Mon, 8 Jul 2024 14:44:37 +0100 Subject: [PATCH 1/4] Opsgenie team responders support (#43535) Signed-off-by: Edward Dowling Co-authored-by: Carlos Castro --- api/types/constants.go | 2 + .../access-request-plugins/opsgenie.mdx | 3 ++ integrations/access/opsgenie/app.go | 45 +++++++++++++----- integrations/access/opsgenie/client.go | 21 +++++++-- integrations/access/opsgenie/client_test.go | 10 ++-- integrations/access/opsgenie/testlib/suite.go | 47 ++++++++++++++++--- 6 files changed, 104 insertions(+), 24 deletions(-) diff --git a/api/types/constants.go b/api/types/constants.go index 47b018f2a0ca1..87c2de6370a1f 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -737,6 +737,8 @@ const ( ReqAnnotationApproveSchedulesLabel = "/schedules" // ReqAnnotationNotifySchedulesLabel is the request annotation key at which notify schedules are stored for access plugins. ReqAnnotationNotifySchedulesLabel = "/notify-services" + // ReqAnnotationTeamsLabel is the request annotation key at which teams are stored for access plugins. + ReqAnnotationTeamsLabel = "/teams" // CloudAWS identifies that a resource was discovered in AWS. CloudAWS = "AWS" diff --git a/docs/pages/access-controls/access-request-plugins/opsgenie.mdx b/docs/pages/access-controls/access-request-plugins/opsgenie.mdx index 3c729c19ecef3..289b9263ef225 100644 --- a/docs/pages/access-controls/access-request-plugins/opsgenie.mdx +++ b/docs/pages/access-controls/access-request-plugins/opsgenie.mdx @@ -74,10 +74,13 @@ spec: deny: 1 annotations: teleport.dev/notify-services: ['teleport-access-request-notifications'] + teleport.dev/teams: ['teleport-team'] teleport.dev/schedules: ['teleport-access-alert-schedules'] ``` The `teleport.dev/notify-services` annotation specifies the schedules the alert will be created for. +The `teleport.dev/teams` annotation specifies the teams the alert will be created for. This is useful when you +have multiple schedules with escalations or an Opsgenie integration that only works with teams. The `teleport.dev/schedules` annotation specifies the schedules the alert will check, and auto approve the Access Request if the requesting user is on-call. diff --git a/integrations/access/opsgenie/app.go b/integrations/access/opsgenie/app.go index 783dc1f70a3dc..c81ecb39f4a8f 100644 --- a/integrations/access/opsgenie/app.go +++ b/integrations/access/opsgenie/app.go @@ -275,9 +275,34 @@ func (a *App) onDeletedRequest(ctx context.Context, reqID string) error { return a.resolveAlert(ctx, reqID, Resolution{Tag: ResolvedExpired}) } -func (a *App) getNotifyServiceNames(req types.AccessRequest) ([]string, error) { - annotationKey := types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel - return common.GetServiceNamesFromAnnotations(req, annotationKey) +// Get services to notify from both annotations: /notify-services and /teams +// Return error if both are empty +func (a *App) getNotifyServiceNames(ctx context.Context, req types.AccessRequest) ([]string, error) { + log := logger.Get(ctx) + + var servicesNames []string + + scheduleAnnotationKey := types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel + schedules, err := common.GetServiceNamesFromAnnotations(req, scheduleAnnotationKey) + if err != nil { + log.Debugf("No schedules to notifiy in %s", scheduleAnnotationKey) + } else { + servicesNames = append(servicesNames, schedules...) + } + + teamAnnotationKey := types.TeleportNamespace + types.ReqAnnotationTeamsLabel + teams, err := common.GetServiceNamesFromAnnotations(req, teamAnnotationKey) + if err != nil { + log.Debugf("No teams to notifiy in %s", teamAnnotationKey) + } else { + servicesNames = append(servicesNames, teams...) + } + + if len(servicesNames) == 0 { + return nil, trace.NotFound("no services to notify") + } + + return servicesNames, nil } func (a *App) getOnCallServiceNames(req types.AccessRequest) ([]string, error) { @@ -288,8 +313,8 @@ func (a *App) getOnCallServiceNames(req types.AccessRequest) ([]string, error) { func (a *App) tryNotifyService(ctx context.Context, req types.AccessRequest) (bool, error) { log := logger.Get(ctx) - serviceNames, err := a.getNotifyServiceNames(req) - if err != nil { + serviceNames, err := a.getNotifyServiceNames(ctx, req) + if err != nil || len(serviceNames) == 0 { log.Debugf("Skipping the notification: %s", err) return false, trace.Wrap(errMissingAnnotation) } @@ -319,12 +344,8 @@ func (a *App) tryNotifyService(ctx context.Context, req types.AccessRequest) (bo } if isNew { - for _, serviceName := range serviceNames { - alertCtx, _ := logger.WithField(ctx, "opsgenie_service_name", serviceName) - - if err = a.createAlert(alertCtx, serviceName, reqID, reqData); err != nil { - return isNew, trace.Wrap(err, "creating Opsgenie alert") - } + if err = a.createAlert(ctx, reqID, reqData); err != nil { + return isNew, trace.Wrap(err, "creating Opsgenie alert") } if reqReviews := req.GetReviews(); len(reqReviews) > 0 { @@ -337,7 +358,7 @@ func (a *App) tryNotifyService(ctx context.Context, req types.AccessRequest) (bo } // createAlert posts an alert with request information. -func (a *App) createAlert(ctx context.Context, serviceID, reqID string, reqData RequestData) error { +func (a *App) createAlert(ctx context.Context, reqID string, reqData RequestData) error { data, err := a.opsgenie.CreateAlert(ctx, reqID, reqData) if err != nil { return trace.Wrap(err) diff --git a/integrations/access/opsgenie/client.go b/integrations/access/opsgenie/client.go index cc78b65298bbc..d44a892029232 100644 --- a/integrations/access/opsgenie/client.go +++ b/integrations/access/opsgenie/client.go @@ -45,6 +45,7 @@ const ( heartbeatName = "teleport-access-heartbeat" ResponderTypeSchedule = "schedule" ResponderTypeUser = "user" + ResponderTypeTeam = "team" ResolveAlertRequestRetryInterval = time.Second * 10 ResolveAlertRequestRetryTimeout = time.Minute * 2 @@ -82,6 +83,8 @@ type ClientConfig struct { APIEndpoint string // DefaultSchedules are the default on-call schedules to check for auto approval DefaultSchedules []string + // DefaultTeams are the default Opsgenie Teams to add as responders + DefaultTeams []string // Priority is the priority alerts are to be created with Priority string @@ -142,7 +145,7 @@ func (og Client) CreateAlert(ctx context.Context, reqID string, reqData RequestD Message: fmt.Sprintf("Access request from %s", reqData.User), Alias: fmt.Sprintf("%s/%s", alertKeyPrefix, reqID), Description: bodyDetails, - Responders: og.getScheduleResponders(reqData), + Responders: og.getResponders(reqData), Priority: og.Priority, } @@ -205,16 +208,28 @@ func (og Client) getAlertRequestResult(ctx context.Context, reqID string) (GetAl return result, nil } -func (og Client) getScheduleResponders(reqData RequestData) []Responder { +func (og Client) getResponders(reqData RequestData) []Responder { schedules := og.DefaultSchedules if reqSchedules, ok := reqData.SystemAnnotations[types.TeleportNamespace+types.ReqAnnotationNotifySchedulesLabel]; ok { schedules = reqSchedules } - responders := make([]Responder, 0, len(schedules)) + teams := og.DefaultTeams + if reqTeams, ok := reqData.SystemAnnotations[types.TeleportNamespace+types.ReqAnnotationTeamsLabel]; ok { + teams = reqTeams + } + responders := make([]Responder, 0, len(schedules)+len(teams)) for _, s := range schedules { responders = append(responders, Responder{ Type: ResponderTypeSchedule, ID: s, + Name: s, + }) + } + for _, t := range teams { + responders = append(responders, Responder{ + Type: ResponderTypeTeam, + ID: t, + Name: t, }) } return responders diff --git a/integrations/access/opsgenie/client_test.go b/integrations/access/opsgenie/client_test.go index 04e560bb27423..e61e5004c0bcf 100644 --- a/integrations/access/opsgenie/client_test.go +++ b/integrations/access/opsgenie/client_test.go @@ -59,7 +59,8 @@ func TestCreateAlert(t *testing.T) { Roles: []string{"role1", "role2"}, RequestReason: "someReason", SystemAnnotations: types.Labels{ - types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel: {"responder@teleport.com"}, + types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel: {"responder@example.com"}, + types.TeleportNamespace + types.ReqAnnotationTeamsLabel: {"MyOpsGenieTeam"}, }, }) assert.NoError(t, err) @@ -68,8 +69,11 @@ func TestCreateAlert(t *testing.T) { Message: "Access request from someUser", Alias: "teleport-access-request/someRequestID", Description: "someUser requested permissions for roles role1, role2 on Teleport at 01 Jan 01 00:00 UTC.\nReason: someReason\n\n", - Responders: []Responder{{Type: "schedule", ID: "responder@teleport.com"}}, - Priority: "somePriority", + Responders: []Responder{ + {Type: "schedule", Name: "responder@example.com", ID: "responder@example.com"}, + {Type: "team", Name: "MyOpsGenieTeam", ID: "MyOpsGenieTeam"}, + }, + Priority: "somePriority", } var got AlertBody err = json.Unmarshal([]byte(recievedReq), &got) diff --git a/integrations/access/opsgenie/testlib/suite.go b/integrations/access/opsgenie/testlib/suite.go index e3917fc51ac99..5b46037a861a5 100644 --- a/integrations/access/opsgenie/testlib/suite.go +++ b/integrations/access/opsgenie/testlib/suite.go @@ -37,10 +37,13 @@ const ( ResponderName1 = "Responder 1" ResponderName2 = "Responder 2" ResponderName3 = "Responder 3" - NotifyScheduleName = "Teleport Notifications" + NotifyScheduleNameOne = "Teleport Notifications One" + NotifyScheduleNameTwo = "Teleport Notifications Two" NotifyScheduleAnnotation = types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel ApprovalScheduleName = "Teleport Approval" ApprovalScheduleAnnotation = types.TeleportNamespace + types.ReqAnnotationApproveSchedulesLabel + NotifyTeamName = "My Team" + NotifyTeamAnnotation = types.TeleportNamespace + types.ReqAnnotationTeamsLabel ) // OpsgenieBaseSuite is the OpsGenie access plugin test suite. @@ -74,12 +77,13 @@ func (s *OpsgenieBaseSuite) SetupTest() { // This service should be notified for every access request. s.ogNotifyResponder = s.fakeOpsgenie.StoreResponder(opsgenie.Responder{ - Name: NotifyScheduleName, + Name: NotifyScheduleNameOne, }) + s.AnnotateRequesterRoleAccessRequests( ctx, NotifyScheduleAnnotation, - []string{NotifyScheduleName}, + []string{NotifyScheduleNameOne, NotifyScheduleNameTwo}, ) // Responder 1 and 2 are on-call and should be automatically approved. @@ -136,9 +140,9 @@ func (s *OpsgenieSuiteEnterprise) SetupTest() { s.OpsgenieBaseSuite.SetupTest() } -// TestAlertCreation validates that an alert is created to the service -// specified in the role's annotation. -func (s *OpsgenieSuiteOSS) TestAlertCreation() { +// TestAlertCreationForSchedules validates that an alert is created to the service +// specified in the role's annotation using /notify-services annotation +func (s *OpsgenieSuiteOSS) TestAlertCreationForSchedules() { t := s.T() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) @@ -154,6 +158,37 @@ func (s *OpsgenieSuiteOSS) TestAlertCreation() { return data.AlertID != "" }) + alert, err := s.fakeOpsgenie.CheckNewAlert(ctx) + + require.NoError(t, err, "no new alerts stored") + + assert.Equal(t, alert.ID, pluginData.AlertID) +} + +// TestAlertCreationForTeams validates that an alert is created to the service +// specified in the role's annotation using /teams annotation +func (s *OpsgenieSuiteOSS) TestAlertCreationForTeams() { + t := s.T() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + s.AnnotateRequesterRoleAccessRequests( + ctx, + NotifyTeamAnnotation, + []string{NotifyTeamName}, + ) + + s.startApp() + + // Test execution: create an access request + req := s.CreateAccessRequest(ctx, integration.RequesterOSSUserName, nil) + + // Validate the alert has been created in OpsGenie and its ID is stored in + // the plugin_data. + pluginData := s.checkPluginData(ctx, req.GetName(), func(data opsgenie.PluginData) bool { + return data.AlertID != "" + }) + alert, err := s.fakeOpsgenie.CheckNewAlert(ctx) require.NoError(t, err, "no new alerts stored") From be36ab223df5c5765ea062c64592c0c339a755a0 Mon Sep 17 00:00:00 2001 From: Edward Dowling Date: Wed, 17 Jul 2024 16:25:52 +0100 Subject: [PATCH 2/4] Split schedules/teams into seperate alerts --- integrations/access/opsgenie/app.go | 10 +++---- integrations/access/opsgenie/testlib/suite.go | 26 ++++++++++++++++--- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/integrations/access/opsgenie/app.go b/integrations/access/opsgenie/app.go index c81ecb39f4a8f..d3fe48ebebd38 100644 --- a/integrations/access/opsgenie/app.go +++ b/integrations/access/opsgenie/app.go @@ -344,13 +344,11 @@ func (a *App) tryNotifyService(ctx context.Context, req types.AccessRequest) (bo } if isNew { - if err = a.createAlert(ctx, reqID, reqData); err != nil { - return isNew, trace.Wrap(err, "creating Opsgenie alert") - } + for _, serviceName := range serviceNames { + alertCtx, _ := logger.WithField(ctx, "opsgenie_service_name", serviceName) - if reqReviews := req.GetReviews(); len(reqReviews) > 0 { - if err = a.postReviewNotes(ctx, reqID, reqReviews); err != nil { - return isNew, trace.Wrap(err) + if err = a.createAlert(alertCtx, reqID, reqData); err != nil { + return isNew, trace.Wrap(err, "creating Opsgenie alert") } } } diff --git a/integrations/access/opsgenie/testlib/suite.go b/integrations/access/opsgenie/testlib/suite.go index 5b46037a861a5..d76ceff60ddd8 100644 --- a/integrations/access/opsgenie/testlib/suite.go +++ b/integrations/access/opsgenie/testlib/suite.go @@ -37,8 +37,7 @@ const ( ResponderName1 = "Responder 1" ResponderName2 = "Responder 2" ResponderName3 = "Responder 3" - NotifyScheduleNameOne = "Teleport Notifications One" - NotifyScheduleNameTwo = "Teleport Notifications Two" + NotifyScheduleName = "Teleport Notifications" NotifyScheduleAnnotation = types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel ApprovalScheduleName = "Teleport Approval" ApprovalScheduleAnnotation = types.TeleportNamespace + types.ReqAnnotationApproveSchedulesLabel @@ -77,13 +76,13 @@ func (s *OpsgenieBaseSuite) SetupTest() { // This service should be notified for every access request. s.ogNotifyResponder = s.fakeOpsgenie.StoreResponder(opsgenie.Responder{ - Name: NotifyScheduleNameOne, + Name: NotifyScheduleName, }) s.AnnotateRequesterRoleAccessRequests( ctx, NotifyScheduleAnnotation, - []string{NotifyScheduleNameOne, NotifyScheduleNameTwo}, + []string{NotifyScheduleName}, ) // Responder 1 and 2 are on-call and should be automatically approved. @@ -172,6 +171,12 @@ func (s *OpsgenieSuiteOSS) TestAlertCreationForTeams() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) t.Cleanup(cancel) + s.AnnotateRequesterRoleAccessRequests( + ctx, + NotifyScheduleAnnotation, + []string{}, + ) + s.AnnotateRequesterRoleAccessRequests( ctx, NotifyTeamAnnotation, @@ -193,6 +198,19 @@ func (s *OpsgenieSuiteOSS) TestAlertCreationForTeams() { require.NoError(t, err, "no new alerts stored") assert.Equal(t, alert.ID, pluginData.AlertID) + + // Reset annotations + s.AnnotateRequesterRoleAccessRequests( + ctx, + NotifyScheduleAnnotation, + []string{NotifyScheduleName}, + ) + + s.AnnotateRequesterRoleAccessRequests( + ctx, + NotifyTeamAnnotation, + []string{}, + ) } // TestApproval tests that when a request is approved, its corresponding alert From 3d594eb4fc0af2aaaf0d2d561b27a3086a50291d Mon Sep 17 00:00:00 2001 From: Edward Dowling Date: Mon, 29 Jul 2024 09:32:19 +0100 Subject: [PATCH 3/4] Update integrations/access/opsgenie/app.go Co-authored-by: Nic Klaassen --- integrations/access/opsgenie/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/access/opsgenie/app.go b/integrations/access/opsgenie/app.go index d3fe48ebebd38..a7d2e5c03cf2d 100644 --- a/integrations/access/opsgenie/app.go +++ b/integrations/access/opsgenie/app.go @@ -285,7 +285,7 @@ func (a *App) getNotifyServiceNames(ctx context.Context, req types.AccessRequest scheduleAnnotationKey := types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel schedules, err := common.GetServiceNamesFromAnnotations(req, scheduleAnnotationKey) if err != nil { - log.Debugf("No schedules to notifiy in %s", scheduleAnnotationKey) + log.Debugf("No schedules to notify in %s", scheduleAnnotationKey) } else { servicesNames = append(servicesNames, schedules...) } From 0005e4bd055f07f97903b686c908ec84d91582b3 Mon Sep 17 00:00:00 2001 From: Edward Dowling Date: Mon, 29 Jul 2024 09:32:26 +0100 Subject: [PATCH 4/4] aUpdate integrations/access/opsgenie/app.go Co-authored-by: Nic Klaassen --- integrations/access/opsgenie/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/access/opsgenie/app.go b/integrations/access/opsgenie/app.go index a7d2e5c03cf2d..50e70553a8778 100644 --- a/integrations/access/opsgenie/app.go +++ b/integrations/access/opsgenie/app.go @@ -293,7 +293,7 @@ func (a *App) getNotifyServiceNames(ctx context.Context, req types.AccessRequest teamAnnotationKey := types.TeleportNamespace + types.ReqAnnotationTeamsLabel teams, err := common.GetServiceNamesFromAnnotations(req, teamAnnotationKey) if err != nil { - log.Debugf("No teams to notifiy in %s", teamAnnotationKey) + log.Debugf("No teams to notify in %s", teamAnnotationKey) } else { servicesNames = append(servicesNames, teams...) }