Skip to content

Commit

Permalink
periodically create access list reminder notifications when needed (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
rudream committed Jan 31, 2025
1 parent 0490a5e commit 1b1283d
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 8 deletions.
10 changes: 5 additions & 5 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -1167,9 +1167,6 @@ const (
// NotificationAccessRequestPromotedSubKind is the subkind for a notification for a user's access request being promoted to an access list.
NotificationAccessRequestPromotedSubKind = "access-request-promoted"

// NotificationAccessListReviewDue30dSubKind is the subkind for a notification for an access list review due in less than 30 days.
NotificationAccessListReviewDue30dSubKind = "access-list-review-due-30d"

// NotificationAccessListReviewDue14dSubKind is the subkind for a notification for an access list review due in less than 14 days.
NotificationAccessListReviewDue14dSubKind = "access-list-review-due-14d"

Expand All @@ -1179,6 +1176,9 @@ const (
// NotificationAccessListReviewDue3dSubKind is the subkind for a notification for an access list review due in less than 3 days.
NotificationAccessListReviewDue3dSubKind = "access-list-review-due-3d"

// NotificationAccessListReviewDue0dSubKind is the subkind for a notification for an access list review due today.
NotificationAccessListReviewDue0dSubKind = "access-list-review-due-0d"

// NotificationAccessListReviewOverdue3dSubKind is the subkind for a notification for an access list review overdue by 3 days.
NotificationAccessListReviewOverdue3dSubKind = "access-list-review-overdue-3d"

Expand All @@ -1187,14 +1187,14 @@ const (
)

const (
// NotificationIdentifierPrefixAccessListDueReminder30d is the prefix for unique notification identifiers for 30d access list review reminders.
NotificationIdentifierPrefixAccessListDueReminder30d = "access_list_30d_due_reminder"
// NotificationIdentifierPrefixAccessListDueReminder14d is the prefix for unique notification identifiers for 14d access list review reminders.
NotificationIdentifierPrefixAccessListDueReminder14d = "access_list_14d_due_reminder"
// NotificationIdentifierPrefixAccessListDueReminder7d is the prefix for unique notification identifiers for 7d access list review reminders.
NotificationIdentifierPrefixAccessListDueReminder7d = "access_list_7d_due_reminder"
// NotificationIdentifierPrefixAccessListDueReminder3d is the prefix for unique notification identifiers for 3d access list review reminders.
NotificationIdentifierPrefixAccessListDueReminder3d = "access_list_3d_due_reminder"
// NotificationIdentifierPrefixAccessListDueReminder0d is the prefix for unique notification identifiers for 0d (today) access list review reminders.
NotificationIdentifierPrefixAccessListDueReminder0d = "access_list_0d_due_reminder"
// NotificationIdentifierPrefixAccessListDueReminder30d is the prefix for unique notification identifiers for 3d overdue access list review reminders.
NotificationIdentifierPrefixAccessListOverdue3d = "access_list_3d_overdue_reminder"
// NotificationIdentifierPrefixAccessListDueReminder30d is the prefix for unique notification identifiers for 7d overdue access list review reminders.
Expand Down
4 changes: 4 additions & 0 deletions api/types/semaphore.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const SemaphoreKindAccessMonitoringLimiter = "access_monitoring_limiter"
// session recordings backend.
const SemaphoreKindUploadCompleter = "upload_completer"

// SemaphoreKindAccessListReminderLimiter is the semaphore kind used by
// the periodic check which creates access list reminder notifications.
const SemaphoreKindAccessListReminderLimiter = "access_list_reminder_limiter"

// Semaphore represents distributed semaphore concept
type Semaphore interface {
// Resource contains common resource values
Expand Down
242 changes: 242 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import (
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/types/wrappers"
apiutils "github.com/gravitational/teleport/api/utils"
Expand Down Expand Up @@ -166,6 +167,7 @@ const (
const (
notificationsPageReadInterval = 5 * time.Millisecond
notificationsWriteInterval = 40 * time.Millisecond
accessListsPageReadInterval = 5 * time.Millisecond
)

var ErrRequiresEnterprise = services.ErrRequiresEnterprise
Expand Down Expand Up @@ -1351,6 +1353,7 @@ const (
desktopCheckKey
upgradeWindowCheckKey
roleCountKey
accessListReminderNotificationsKey
)

// runPeriodicOperations runs some periodic bookkeeping operations
Expand Down Expand Up @@ -1400,6 +1403,12 @@ func (a *Server) runPeriodicOperations() {
FirstDuration: retryutils.FullJitter(time.Minute),
Jitter: retryutils.SeventhJitter,
},
interval.SubInterval[periodicIntervalKey]{
Key: accessListReminderNotificationsKey,
Duration: 8 * time.Hour,
FirstDuration: retryutils.FullJitter(time.Hour),
Jitter: retryutils.SeventhJitter,
},
)

defer ticker.Stop()
Expand Down Expand Up @@ -1572,6 +1581,8 @@ func (a *Server) runPeriodicOperations() {
go a.syncUpgradeWindowStartHour(a.closeCtx)
case roleCountKey:
go a.tallyRoles(a.closeCtx)
case accessListReminderNotificationsKey:
go a.CreateAccessListReminderNotifications(a.closeCtx)
}
}
}
Expand Down Expand Up @@ -6124,6 +6135,237 @@ func (a *Server) CleanupNotifications(ctx context.Context) {
}
}

const (
accessListReminderSemaphoreName = "access-list-reminder-check"
accessListReminderSemaphoreMaxLeases = 1
)

// CreateAccessListReminderNotifications checks if there are any access lists expiring soon and creates notifications to remind their owners if so.
func (a *Server) CreateAccessListReminderNotifications(ctx context.Context) {
// Ensure only one auth server is running this check at a time.
lease, err := services.AcquireSemaphoreLock(ctx, services.SemaphoreLockConfig{
Service: a,
Clock: a.clock,
Expiry: 5 * time.Minute,
Params: types.AcquireSemaphoreRequest{
SemaphoreKind: types.SemaphoreKindAccessListReminderLimiter,
SemaphoreName: accessListReminderSemaphoreName,
MaxLeases: accessListReminderSemaphoreMaxLeases,
Holder: a.ServerID,
},
})
if err != nil {
a.logger.WarnContext(ctx, "unable to acquire semaphore, will skip this access list reminder check", "server_id", a.ServerID)
return
}

defer func() {
lease.Stop()
if err := lease.Wait(); err != nil {
a.logger.WarnContext(ctx, "error cleaning up semaphore", "error", err)
}
}()

now := a.clock.Now()

// Fetch all access lists
var accessLists []*accesslist.AccessList
var accessListsPageKey string
accessListsReadLimiter := time.NewTicker(accessListsPageReadInterval)
defer accessListsReadLimiter.Stop()
for {
select {
case <-accessListsReadLimiter.C:
case <-ctx.Done():
return
}
response, nextKey, err := a.Cache.ListAccessLists(ctx, 20, accessListsPageKey)
if err != nil {
a.logger.WarnContext(ctx, "failed to list access lists for periodic reminder notification check", "error", err)
}

for _, al := range response {
daysDiff := int(al.Spec.Audit.NextAuditDate.Sub(now).Hours() / 24)
// Only keep access lists that fall within our thresholds in memory
if daysDiff <= 15 && daysDiff >= -8 {
accessLists = append(accessLists, al)
}
}

if nextKey == "" {
break
}
accessListsPageKey = nextKey
}

reminderThresholds := []struct {
days int
prefix string
notificationSubkind string
}{
{14, types.NotificationIdentifierPrefixAccessListDueReminder14d, types.NotificationAccessListReviewDue14dSubKind},
{7, types.NotificationIdentifierPrefixAccessListDueReminder7d, types.NotificationAccessListReviewDue7dSubKind},
{3, types.NotificationIdentifierPrefixAccessListDueReminder3d, types.NotificationAccessListReviewDue3dSubKind},
{0, types.NotificationIdentifierPrefixAccessListDueReminder0d, types.NotificationAccessListReviewDue0dSubKind},
{-3, types.NotificationIdentifierPrefixAccessListOverdue3d, types.NotificationAccessListReviewOverdue3dSubKind},
{-7, types.NotificationIdentifierPrefixAccessListOverdue7d, types.NotificationAccessListReviewOverdue7dSubKind},
}

for _, threshold := range reminderThresholds {
var relevantLists []*accesslist.AccessList

// Filter access lists based on due date
for _, al := range accessLists {
dueDate := al.Spec.Audit.NextAuditDate
timeDiff := dueDate.Sub(now)
daysDiff := int(timeDiff.Hours() / 24)

if threshold.days < 0 {
if daysDiff <= threshold.days {
relevantLists = append(relevantLists, al)
}
} else {
if daysDiff >= 0 && daysDiff <= threshold.days {
relevantLists = append(relevantLists, al)
}
}
}

if len(relevantLists) == 0 {
continue
}

// Fetch all identifiers for this treshold prefix.
var identifiers []*notificationsv1.UniqueNotificationIdentifier
var nextKey string
for {
identifiersResp, nextKey, err := a.ListUniqueNotificationIdentifiersForPrefix(ctx, threshold.prefix, 0, nextKey)
if err != nil {
a.logger.WarnContext(ctx, "failed to list notification identifiers", "error", err, "prefix", threshold.prefix)
continue
}
identifiers = append(identifiers, identifiersResp...)
if nextKey == "" {
break
}
}

// Create a map of identifiers for quick lookup
identifiersMap := make(map[string]struct{})
for _, id := range identifiers {
// id.Spec.UniqueIdentifier is the access list ID
identifiersMap[id.Spec.UniqueIdentifier] = struct{}{}
}

// owners is the combined list of owners for relevant access lists we are creating the notification for.
var owners []string

// Check for access lists which haven't already been accounted for in a notification
var needsNotification bool

writeLimiter := time.NewTicker(notificationsWriteInterval)
for _, accessList := range relevantLists {
select {
case <-writeLimiter.C:
case <-ctx.Done():
return
}

if _, exists := identifiersMap[accessList.GetName()]; !exists {
needsNotification = true
// Create a unique identifier for this access list so that we know it has been accounted for.
// Note that if the auth server crashes between creating this identifier and creating the notification,
// the notification will be missed. This has been judged as an acceptable outcome for access lists,
// but the same strategy may not be acceptable for other notification types.
if _, err := a.CreateUniqueNotificationIdentifier(ctx, threshold.prefix, accessList.GetName()); err != nil {
a.logger.WarnContext(ctx, "failed to create notification identifier", "error", err, "access_list", accessList.GetName())
continue
}
for _, owner := range accessList.Spec.Owners {
owners = append(owners, owner.Name)
}
}
}
writeLimiter.Stop()

owners = apiutils.Deduplicate(owners)

var title string
if threshold.days == 0 {
title = "You have access lists due for review today."
} else if threshold.days < 0 {
title = fmt.Sprintf("You have access lists that are more than %d days overdue for review", -threshold.days)
} else {
title = fmt.Sprintf("You have access lists due for review in less than %d days.", threshold.days)
}

// Create the notification for this reminder treshold for all relevant owners.
if needsNotification {
err := a.createAccessListReminderNotification(ctx, owners, threshold.notificationSubkind, title)
if err != nil {
a.logger.WarnContext(ctx, "Failed to create access list reminder notification", "error", err)
}
}
}
}

// createAccessListReminderNotification is a helper function to create a notification for an access list reminder.
func (a *Server) createAccessListReminderNotification(ctx context.Context, owners []string, subkind string, title string) error {
_, err := a.Services.CreateGlobalNotification(ctx, &notificationsv1.GlobalNotification{
Spec: &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByUsers{
ByUsers: &notificationsv1.ByUsers{
Users: owners,
},
},
Notification: &notificationsv1.Notification{
Spec: &notificationsv1.NotificationSpec{},
SubKind: subkind,
Metadata: &headerv1.Metadata{
Labels: map[string]string{types.NotificationTitleLabel: title},
},
},
},
})
if err != nil {
return err
}

// Also create a notification for users who have CRUD permissions for access lists. This is because they can also review access lists.
_, err = a.Services.CreateGlobalNotification(ctx, &notificationsv1.GlobalNotification{
Spec: &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByPermissions{
ByPermissions: &notificationsv1.ByPermissions{
RoleConditions: []*types.RoleConditions{
{
Rules: []types.Rule{
{
Resources: []string{types.KindAccessList},
Verbs: services.RW(),
},
},
},
},
},
},
// Exclude the list of owners so that they don't get a duplicate notification, since we already created a notification for them.
ExcludeUsers: owners,
Notification: &notificationsv1.Notification{
Spec: &notificationsv1.NotificationSpec{},
SubKind: subkind,
Metadata: &headerv1.Metadata{
Labels: map[string]string{types.NotificationTitleLabel: title},
},
},
},
})
if err != nil {
return err
}

return nil
}

// GenerateCertAuthorityCRL generates an empty CRL for the local CA of a given type.
func (a *Server) GenerateCertAuthorityCRL(ctx context.Context, caType types.CertAuthType) ([]byte, error) {
// Generate a CRL for the current cluster CA.
Expand Down
Loading

0 comments on commit 1b1283d

Please sign in to comment.