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

[v15] Opsgenie team responders support #44330

Merged
merged 6 commits into from
Jul 29, 2024
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
2 changes: 2 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
45 changes: 32 additions & 13 deletions integrations/access/opsgenie/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 notify 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 notify 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) {
Expand All @@ -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)
}
Expand Down Expand Up @@ -322,22 +347,16 @@ func (a *App) tryNotifyService(ctx context.Context, req types.AccessRequest) (bo
for _, serviceName := range serviceNames {
alertCtx, _ := logger.WithField(ctx, "opsgenie_service_name", serviceName)

if err = a.createAlert(alertCtx, serviceName, reqID, reqData); err != nil {
if err = a.createAlert(alertCtx, reqID, reqData); err != nil {
return isNew, trace.Wrap(err, "creating Opsgenie alert")
}
}

if reqReviews := req.GetReviews(); len(reqReviews) > 0 {
if err = a.postReviewNotes(ctx, reqID, reqReviews); err != nil {
return isNew, trace.Wrap(err)
}
}
}
return isNew, nil
}

// 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)
Expand Down
21 changes: 18 additions & 3 deletions integrations/access/opsgenie/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
heartbeatName = "teleport-access-heartbeat"
ResponderTypeSchedule = "schedule"
ResponderTypeUser = "user"
ResponderTypeTeam = "team"

ResolveAlertRequestRetryInterval = time.Second * 10
ResolveAlertRequestRetryTimeout = time.Minute * 2
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions integrations/access/opsgenie/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ func TestCreateAlert(t *testing.T) {
Roles: []string{"role1", "role2"},
RequestReason: "someReason",
SystemAnnotations: types.Labels{
types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel: {"[email protected]"},
types.TeleportNamespace + types.ReqAnnotationNotifySchedulesLabel: {"[email protected]"},
types.TeleportNamespace + types.ReqAnnotationTeamsLabel: {"MyOpsGenieTeam"},
},
})
assert.NoError(t, err)
Expand All @@ -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: "[email protected]"}},
Priority: "somePriority",
Responders: []Responder{
{Type: "schedule", Name: "[email protected]", ID: "[email protected]"},
{Type: "team", Name: "MyOpsGenieTeam", ID: "MyOpsGenieTeam"},
},
Priority: "somePriority",
}
var got AlertBody
err = json.Unmarshal([]byte(recievedReq), &got)
Expand Down
59 changes: 56 additions & 3 deletions integrations/access/opsgenie/testlib/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const (
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.
Expand Down Expand Up @@ -76,6 +78,7 @@ func (s *OpsgenieBaseSuite) SetupTest() {
s.ogNotifyResponder = s.fakeOpsgenie.StoreResponder(opsgenie.Responder{
Name: NotifyScheduleName,
})

s.AnnotateRequesterRoleAccessRequests(
ctx,
NotifyScheduleAnnotation,
Expand Down Expand Up @@ -136,13 +139,50 @@ 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)

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")

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,
NotifyScheduleAnnotation,
[]string{},
)

s.AnnotateRequesterRoleAccessRequests(
ctx,
NotifyTeamAnnotation,
[]string{NotifyTeamName},
)

s.startApp()

// Test execution: create an access request
Expand All @@ -158,6 +198,19 @@ func (s *OpsgenieSuiteOSS) TestAlertCreation() {
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
Expand Down
Loading