From fa24b825bb25145baa6801e63e777f5c800b4bca Mon Sep 17 00:00:00 2001 From: Yassine Bounekhla Date: Wed, 5 Jun 2024 20:57:06 -0400 Subject: [PATCH] add support for access request promoted notification --- api/types/constants.go | 2 + lib/auth/auth.go | 26 ++-- lib/auth/auth_test.go | 116 ++++++++++++++++++ .../teleport/src/Notifications/fixtures.ts | 13 ++ .../src/services/notifications/types.ts | 1 + 5 files changed, 148 insertions(+), 10 deletions(-) diff --git a/api/types/constants.go b/api/types/constants.go index 021474c909e32..a217e3f680150 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -1029,6 +1029,8 @@ const ( NotificationAccessRequestApprovedSubKind = "access-request-approved" // NotificationAccessRequestDeniedSubKind is the subkind for a notification for a user's access request being denied. 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" ) const ( diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 727373a8b9014..726552d56f936 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -5202,23 +5202,29 @@ func generateAccessRequestReviewedNotification(req types.AccessRequest, params t if req.GetState().IsApproved() { subKind = types.NotificationAccessRequestApprovedSubKind reviewVerb = "approved" + } else if req.GetState().IsPromoted() { + subKind = types.NotificationAccessRequestPromotedSubKind } else { subKind = types.NotificationAccessRequestDeniedSubKind reviewVerb = "denied" } var notificationText string - // If this was a resource request. - if len(req.GetRequestedResourceIDs()) > 0 { - notificationText = fmt.Sprintf("%s %s your access request for %d resources.", params.Review.Author, reviewVerb, len(req.GetRequestedResourceIDs())) - if len(req.GetRequestedResourceIDs()) == 1 { - notificationText = fmt.Sprintf("%s %s your access request for a resource.", params.Review.Author, reviewVerb) - } - // If this was a role request. + if req.GetState().IsPromoted() { + notificationText = fmt.Sprintf("%s promoted your access request to long-term access.", params.Review.Author) } else { - notificationText = fmt.Sprintf("%s %s your access request for the '%s' role.", params.Review.Author, reviewVerb, req.GetRoles()[0]) - if len(req.GetRoles()) > 1 { - notificationText = fmt.Sprintf("%s %s your access request for %d roles.", params.Review.Author, reviewVerb, len(req.GetRoles())) + // If this was a resource request. + if len(req.GetRequestedResourceIDs()) > 0 { + notificationText = fmt.Sprintf("%s %s your access request for %d resources.", params.Review.Author, reviewVerb, len(req.GetRequestedResourceIDs())) + if len(req.GetRequestedResourceIDs()) == 1 { + notificationText = fmt.Sprintf("%s %s your access request for a resource.", params.Review.Author, reviewVerb) + } + // If this was a role request. + } else { + notificationText = fmt.Sprintf("%s %s your access request for the '%s' role.", params.Review.Author, reviewVerb, req.GetRoles()[0]) + if len(req.GetRoles()) > 1 { + notificationText = fmt.Sprintf("%s %s your access request for %d roles.", params.Review.Author, reviewVerb, len(req.GetRoles())) + } } } diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index 0fa7bc8c03434..ea21740618973 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -3943,3 +3943,119 @@ func TestAccessRequestAuditLog(t *testing.T) { require.Equal(t, expectedAnnotations, arc.Annotations) require.Equal(t, "APPROVED", arc.RequestState) } + +func TestAccessRequestNotifications(t *testing.T) { + t.Parallel() + ctx := context.Background() + + fakeClock := clockwork.NewFakeClock() + + testAuthServer, err := NewTestAuthServer(TestAuthServerConfig{ + Dir: t.TempDir(), + Clock: fakeClock, + }) + require.NoError(t, err) + testTLSServer, err := testAuthServer.NewTestTLSServer() + require.NoError(t, err) + + reviewerUsername := "reviewer" + requesterUsername := "requester" + requestRoleName := "requestRole" + + reviewerRole, err := types.NewRole(reviewerUsername, types.RoleSpecV6{ + Allow: types.RoleConditions{ + Logins: []string{"user"}, + ReviewRequests: &types.AccessReviewConditions{ + Roles: []string{"requestRole"}, + }, + }, + }) + require.NoError(t, err) + + requesterRole, err := types.NewRole(requesterUsername, types.RoleSpecV6{ + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + Roles: []string{requestRoleName}, + }, + }, + }) + require.NoError(t, err) + + requestedRole, err := types.NewRole(requestRoleName, types.RoleSpecV6{ + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + Roles: []string{requestRoleName}, + }, + }, + }) + require.NoError(t, err) + _, err = testTLSServer.AuthServer.AuthServer.UpsertRole(ctx, requestedRole) + require.NoError(t, err) + + _, err = testTLSServer.AuthServer.AuthServer.UpsertRole(ctx, reviewerRole) + require.NoError(t, err) + reviewer, err := types.NewUser(reviewerUsername) + require.NoError(t, err) + reviewer.SetRoles([]string{reviewerUsername}) + _, err = testTLSServer.AuthServer.AuthServer.UpsertUser(ctx, reviewer) + require.NoError(t, err) + + _, err = testTLSServer.AuthServer.AuthServer.UpsertRole(ctx, requesterRole) + require.NoError(t, err) + requester, err := types.NewUser(requesterUsername) + require.NoError(t, err) + requester.SetRoles([]string{requesterUsername}) + _, err = testTLSServer.AuthServer.AuthServer.UpsertUser(ctx, requester) + require.NoError(t, err) + + accessRequest, err := types.NewAccessRequest(uuid.NewString(), requesterUsername, requestRoleName) + require.NoError(t, err) + req, err := testTLSServer.AuthServer.AuthServer.CreateAccessRequestV2(ctx, accessRequest, TestUser(requesterUsername).I.GetIdentity()) + require.NoError(t, err) + + // Verify that a global notification was created which matches for users who can review the requestRole. + globalNotifsResp, _, err := testTLSServer.AuthServer.AuthServer.Notifications.ListGlobalNotifications(ctx, 100, "") + require.NoError(t, err) + require.Len(t, globalNotifsResp, 1) + require.Equal(t, &types.AccessReviewConditions{ + Roles: []string{requestRoleName}, + }, globalNotifsResp[0].GetSpec().GetByPermissions().GetRoleConditions()[0].ReviewRequests) + + reviewerIdentity := TestUser(reviewerUsername) + reviewerClient, err := testTLSServer.NewClient(reviewerIdentity) + require.NoError(t, err) + + // Approve the request + _, err = reviewerClient.SubmitAccessReview(ctx, types.AccessReviewSubmission{ + RequestID: req.GetName(), + Review: types.AccessReview{ + ProposedState: types.RequestState_APPROVED, + }, + }) + require.NoError(t, err) + // Verify that a user notification was created notifying the requester that their access request was approved. + userNotifsResp, _, err := testTLSServer.AuthServer.AuthServer.Notifications.ListUserNotifications(ctx, 100, "") + require.NoError(t, err) + require.Len(t, userNotifsResp, 1) + require.Contains(t, userNotifsResp[0].GetMetadata().GetLabels()[types.NotificationTitleLabel], "reviewer approved your access request") + + // Create another access request. + accessRequest, err = types.NewAccessRequest(uuid.NewString(), requesterUsername, requestRoleName) + require.NoError(t, err) + req, err = testTLSServer.AuthServer.AuthServer.CreateAccessRequestV2(ctx, accessRequest, TestUser(requesterUsername).I.GetIdentity()) + require.NoError(t, err) + + // Deny the request. + _, err = reviewerClient.SubmitAccessReview(ctx, types.AccessReviewSubmission{ + RequestID: req.GetName(), + Review: types.AccessReview{ + ProposedState: types.RequestState_DENIED, + }, + }) + require.NoError(t, err) + // Verify that a user notification was created notifying the requester that their access request was denied. + userNotifsResp, _, err = testTLSServer.AuthServer.AuthServer.Notifications.ListUserNotifications(ctx, 100, "") + require.NoError(t, err) + require.Len(t, userNotifsResp, 2) + require.Contains(t, userNotifsResp[1].GetMetadata().GetLabels()[types.NotificationTitleLabel], "reviewer denied your access request") +} diff --git a/web/packages/teleport/src/Notifications/fixtures.ts b/web/packages/teleport/src/Notifications/fixtures.ts index 4b4f6809098b4..f64483f4bc086 100644 --- a/web/packages/teleport/src/Notifications/fixtures.ts +++ b/web/packages/teleport/src/Notifications/fixtures.ts @@ -100,4 +100,17 @@ export const notifications: Notification[] = [ }, ], }, + { + id: '7', + title: `joe promoted your access request to long-term access.`, + subKind: NotificationSubKind.AccessRequestPromoted, + createdDate: subMinutes(Date.now(), 4), // 4 minutes ago + clicked: false, + labels: [ + { + name: 'request-id', + value: '3bd7d71f-64ad-588a-988c-22f3853910fa', + }, + ], + }, ]; diff --git a/web/packages/teleport/src/services/notifications/types.ts b/web/packages/teleport/src/services/notifications/types.ts index d0c3bc6bb5a65..9573089193183 100644 --- a/web/packages/teleport/src/services/notifications/types.ts +++ b/web/packages/teleport/src/services/notifications/types.ts @@ -93,6 +93,7 @@ export enum NotificationSubKind { AccessRequestPending = 'access-request-pending', AccessRequestApproved = 'access-request-approved', AccessRequestDenied = 'access-request-denied', + AccessRequestPromoted = 'access-request-promoted', } /** 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. */