Skip to content

Commit

Permalink
add access list reminder notification type (#51426)
Browse files Browse the repository at this point in the history
  • Loading branch information
rudream authored Jan 29, 2025
1 parent b31c27e commit c907235
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 185 deletions.
422 changes: 247 additions & 175 deletions api/gen/proto/go/teleport/notifications/v1/notifications.pb.go

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions api/proto/teleport/notifications/v1/notifications.proto
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ message GlobalNotificationSpec {
ByRoles by_roles = 2;
// all represents whether to target all users, regardless of roles or permissions.
bool all = 3;
// by_users represents a list of usernames of the users targeted by this notification.
// If only one user is being targeted, please create a user-specific notification instead.
ByUsers by_users = 7;
}
// match_all_conditions is whether or not all the conditions specified by the matcher must be met,
// if false, only one of the conditions needs to be met.
Expand All @@ -101,6 +104,11 @@ message ByRoles {
repeated string roles = 1;
}

// ByUsers represents the users targeted by this notification.
message ByUsers {
repeated string users = 1;
}

// UserNotificationState represents a notification's state for a user. This is to keep track
// of whether the user has clicked on or dismissed the notification.
message UserNotificationState {
Expand Down
33 changes: 33 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,39 @@ const (
NotificationAccessRequestDeniedSubKind = "access-request-denied"
// 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"

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

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

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

// NotificationAccessListReviewOverdue7dSubKind is the subkind for a notification for an access list review overdue by 7 days.
NotificationAccessListReviewOverdue7dSubKind = "access-list-review-overdue-7d"
)

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"
// 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.
NotificationIdentifierPrefixAccessListOverdue7d = "access_list_7d_overdue_reminder"
)

const (
Expand Down
30 changes: 20 additions & 10 deletions lib/auth/notification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,16 @@ func TestNotifications(t *testing.T) {
},
},
},
{
// Matcher matches by usernames.
globalNotification: newGlobalNotification(t, "auditor-9,manager-9", &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByUsers{
ByUsers: &notificationsv1.ByUsers{
Users: []string{auditorUsername, managerUsername},
},
},
}),
},
}

notificationIdMap := map[string]string{}
Expand Down Expand Up @@ -292,7 +302,7 @@ func TestNotifications(t *testing.T) {
require.NoError(t, err)
defer auditorClient.Close()

auditorExpectedNotifications := []string{"auditor-8,manager-6", "auditor-7", "auditor-6", "auditor-5,manager-2", "auditor-4", "auditor-3", "auditor-2", "auditor-1"}
auditorExpectedNotifications := []string{"auditor-9,manager-9", "auditor-8,manager-6", "auditor-7", "auditor-6", "auditor-5,manager-2", "auditor-4", "auditor-3", "auditor-2", "auditor-1"}

var finalOut []*notificationsv1.Notification

Expand All @@ -315,19 +325,19 @@ func TestNotifications(t *testing.T) {

// Verify that the nextKeys are correct.
expectedUserNotifsNextKey := notificationIdMap["auditor-4"]
expectedGlobalNotifsNextKey := notificationIdMap["auditor-5,manager-2"]
expectedGlobalNotifsNextKey := notificationIdMap["auditor-6"]
expectedNextKeys := fmt.Sprintf("%s,%s",
expectedUserNotifsNextKey,
expectedGlobalNotifsNextKey) // "<auditor-4 notification id>,<auditor-5,manager-2 notification id>"
expectedGlobalNotifsNextKey) // "<auditor-4 notification id>,<auditor-6 notification id>"

require.Equal(t, expectedNextKeys, resp.NextPageToken)
require.Equal(t, lastSeenTimestamp.GetSeconds(), resp.UserLastSeenNotificationTimestamp.GetSeconds())

// Fetch the next 3 notifications, starting from the previously received startKeys.
// Fetch the next 4 notifications, starting from the previously received startKeys.
// After this fetch, there should be no more global notifications for auditor, so the next page token
// for global notifications should be "".
resp, err = auditorClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 3,
PageSize: 4,
PageToken: resp.NextPageToken,
})
require.NoError(t, err)
Expand Down Expand Up @@ -382,7 +392,7 @@ func TestNotifications(t *testing.T) {
resp, err = auditorClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 10,
})
auditorExpectedNotifsAfterDismissal := []string{"auditor-8,manager-6", "auditor-7", "auditor-6", "auditor-4", "auditor-3", "auditor-1"}
auditorExpectedNotifsAfterDismissal := []string{"auditor-9,manager-9", "auditor-8,manager-6", "auditor-7", "auditor-6", "auditor-4", "auditor-3", "auditor-1"}
require.NoError(t, err)
require.Equal(t, auditorExpectedNotifsAfterDismissal, notificationsToTitlesList(t, resp.Notifications))

Expand All @@ -391,7 +401,7 @@ func TestNotifications(t *testing.T) {
require.NoError(t, err)
defer managerClient.Close()

managerExpectedNotifications := []string{"manager-8-expires", "manager-7-expires", "auditor-8,manager-6", "manager-5", "manager-4", "manager-3", "auditor-5,manager-2", "manager-1"}
managerExpectedNotifications := []string{"auditor-9,manager-9", "manager-8-expires", "manager-7-expires", "auditor-8,manager-6", "manager-5", "manager-4", "manager-3", "auditor-5,manager-2", "manager-1"}

resp, err = managerClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 10,
Expand Down Expand Up @@ -419,7 +429,7 @@ func TestNotifications(t *testing.T) {
})
require.NoError(t, err)

clickedNotification := resp.Notifications[0] // "manager-8-expires" is the first item in the list
clickedNotification := resp.Notifications[1] // "manager-8-expires" is the second item in the list
clickedLabelValue := clickedNotification.GetMetadata().GetLabels()[types.NotificationClickedLabel]
require.Equal(t, "true", clickedLabelValue)

Expand All @@ -429,15 +439,15 @@ func TestNotifications(t *testing.T) {
// Verify that notification "manager-8-expires" is now no longer returned.
resp, err = managerClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{})
require.NoError(t, err)
require.Equal(t, managerExpectedNotifications[1:], notificationsToTitlesList(t, resp.Notifications))
require.NotContains(t, notificationsToTitlesList(t, resp.Notifications), "manager-8-expires")

// Advance 16 minutes.
fakeClock.Advance(16 * time.Minute)

// Verify that notification "manager-7-expires" is now no longer returned either.
resp, err = managerClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{})
require.NoError(t, err)
require.Equal(t, managerExpectedNotifications[2:], notificationsToTitlesList(t, resp.Notifications))
require.NotContains(t, notificationsToTitlesList(t, resp.Notifications), "manager-7-expires")

// Verify that manager can't upsert a notification state for auditor
_, err = managerClient.UpsertUserNotificationState(ctx, auditorUsername, &notificationsv1.UserNotificationState{
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/notifications/notificationsv1/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,10 @@ func (s *Service) matchGlobalNotification(ctx context.Context, authCtx *authz.Co
// Always return true if the matcher is "all."
return true

case *notificationsv1.GlobalNotificationSpec_ByUsers:
userList := matcher.ByUsers.GetUsers()
return slices.Contains(userList, authCtx.User.GetName())

case *notificationsv1.GlobalNotificationSpec_ByRoles:
matcherRoles := matcher.ByRoles.GetRoles()
userRoles := authCtx.User.GetRoles()
Expand Down
1 change: 1 addition & 0 deletions web/packages/teleport/src/Notifications/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ function EmptyState() {
);
}

//TODO(rudream): Delete local notifications
/** accessListStoreNotificationsToNotifications converts a list of access list notifications from the notifications store into the primary
* Notification type used by the notifications list.
*/
Expand Down
48 changes: 48 additions & 0 deletions web/packages/teleport/src/Notifications/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,52 @@ export const notifications: Notification[] = [
},
],
},
{
id: '8',
title: `You have access lists that require your review within 30 days.`,
subKind: NotificationSubKind.NotificationAccessListReviewDue30d,
createdDate: subMinutes(Date.now(), 5), // 5 minutes ago
clicked: false,
labels: [],
},
{
id: '9',
title: `You have access lists that require your review within 14 days.`,
subKind: NotificationSubKind.NotificationAccessListReviewDue14d,
createdDate: subMinutes(Date.now(), 6), // 6 minutes ago
clicked: false,
labels: [],
},
{
id: '10',
title: `You have access lists that require your review within 7 days.`,
subKind: NotificationSubKind.NotificationAccessListReviewDue7d,
createdDate: subMinutes(Date.now(), 7), // 7 minutes ago
clicked: false,
labels: [],
},
{
id: '11',
title: `You have access lists that require your review within 3 days.`,
subKind: NotificationSubKind.NotificationAccessListReviewDue3d,
createdDate: subMinutes(Date.now(), 8), // 8 minutes ago
clicked: false,
labels: [],
},
{
id: '12',
title: `You have access lists overdue for review by more than 3 days.`,
subKind: NotificationSubKind.NotificationAccessListReviewOverdue3d,
createdDate: subMinutes(Date.now(), 9), // 9 minutes ago
clicked: false,
labels: [],
},
{
id: '13',
title: `You have access lists overdue for review by more than 7 days.`,
subKind: NotificationSubKind.NotificationAccessListReviewOverdue7d,
createdDate: subMinutes(Date.now(), 10), // 10 minutes ago
clicked: false,
labels: [],
},
];
11 changes: 11 additions & 0 deletions web/packages/teleport/src/services/notifications/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,31 @@ export type Notification = {
export enum NotificationSubKind {
DefaultInformational = 'default-informational',
DefaultWarning = 'default-warning',

UserCreatedInformational = 'user-created-informational',
UserCreatedWarning = 'user-created-warning',

AccessRequestPending = 'access-request-pending',
AccessRequestApproved = 'access-request-approved',
AccessRequestDenied = 'access-request-denied',
AccessRequestPromoted = 'access-request-promoted',

NotificationAccessListReviewDue30d = 'access-list-review-due-30d',
NotificationAccessListReviewDue14d = 'access-list-review-due-14d',
NotificationAccessListReviewDue7d = 'access-list-review-due-7d',
NotificationAccessListReviewDue3d = 'access-list-review-due-3d',
NotificationAccessListReviewOverdue3d = 'access-list-review-overdue-3d',
NotificationAccessListReviewOverdue7d = 'access-list-review-overdue-7d',
}

//TODO(rudream): Delete local notifications
/** LocalNotificationKind is the kind of local notifications which are generated on the frontend and not stored in the backend. These do not need to be kept in sync with the backend. */
export enum LocalNotificationKind {
/** AccessList is a notification for an access list reminder. */
AccessList = 'access-list',
}

//TODO(rudream): Delete local notifications
/** LocalNotificationGroupedKind is the kind of groupings of local notifications. These do not need to be kept in sync with the backend. */
export enum LocalNotificationGroupedKind {
/** AccessListGrouping is a notification which combines the notifications for multiple access list reminders into one to reduce clutter. */
Expand Down

0 comments on commit c907235

Please sign in to comment.