feat: notify when a user account is deleted (#14113)

This commit is contained in:
Marcin Tojek
2024-08-02 14:56:54 +02:00
committed by GitHub
parent 4242fd9c1b
commit 6428a766a9
5 changed files with 138 additions and 14 deletions
@@ -0,0 +1 @@
DELETE FROM notification_templates WHERE id = 'f44d9314-ad03-4bc8-95d0-5cad491da6b6';
@@ -0,0 +1,9 @@
INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions)
VALUES ('f44d9314-ad03-4bc8-95d0-5cad491da6b6', 'User account deleted', E'User account "{{.Labels.deleted_account_name}}" deleted',
E'Hi {{.UserName}},\n\nUser account **{{.Labels.deleted_account_name}}** has been deleted.',
'User Events', '[
{
"label": "View accounts",
"url": "{{ base_url }}/deployment/users?filter=status%3Aactive"
}
]'::jsonb);
+1
View File
@@ -17,4 +17,5 @@ var (
// Account-related events.
var (
TemplateUserAccountCreated = uuid.MustParse("4e19c0ac-94e1-4532-9515-d1801aa283b2")
TemplateUserAccountDeleted = uuid.MustParse("f44d9314-ad03-4bc8-95d0-5cad491da6b6")
)
+43 -14
View File
@@ -567,6 +567,27 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) {
}
user.Deleted = true
aReq.New = user
userAdmins, err := findUserAdmins(ctx, api.Database)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user admins.",
Detail: err.Error(),
})
return
}
for _, u := range userAdmins {
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, notifications.TemplateUserAccountDeleted,
map[string]string{
"deleted_account_name": user.Username,
}, "api-users-delete",
user.ID,
); err != nil {
api.Logger.Warn(ctx, "unable to notify about deleted user", slog.F("deleted_user", user.Username), slog.Error(err))
}
}
rw.WriteHeader(http.StatusNoContent)
}
@@ -1287,23 +1308,12 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
return user, req.OrganizationID, err
}
// Notify all users with user admin permission including owners
// Notice: we can't scrape the user information in parallel as pq
// fails with: unexpected describe rows response: 'D'
owners, err := store.GetUsers(ctx, database.GetUsersParams{
RbacRole: []string{codersdk.RoleOwner},
})
userAdmins, err := findUserAdmins(ctx, store)
if err != nil {
return user, req.OrganizationID, xerrors.Errorf("get owners: %w", err)
}
userAdmins, err := store.GetUsers(ctx, database.GetUsersParams{
RbacRole: []string{codersdk.RoleUserAdmin},
})
if err != nil {
return user, req.OrganizationID, xerrors.Errorf("get user admins: %w", err)
return user, req.OrganizationID, xerrors.Errorf("find user admins: %w", err)
}
for _, u := range append(owners, userAdmins...) {
for _, u := range userAdmins {
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, notifications.TemplateUserAccountCreated,
map[string]string{
"created_account_name": user.Username,
@@ -1316,6 +1326,25 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
return user, req.OrganizationID, err
}
// findUserAdmins fetches all users with user admin permission including owners.
func findUserAdmins(ctx context.Context, store database.Store) ([]database.GetUsersRow, error) {
// Notice: we can't scrape the user information in parallel as pq
// fails with: unexpected describe rows response: 'D'
owners, err := store.GetUsers(ctx, database.GetUsersParams{
RbacRole: []string{codersdk.RoleOwner},
})
if err != nil {
return nil, xerrors.Errorf("get owners: %w", err)
}
userAdmins, err := store.GetUsers(ctx, database.GetUsersParams{
RbacRole: []string{codersdk.RoleUserAdmin},
})
if err != nil {
return nil, xerrors.Errorf("get user admins: %w", err)
}
return append(owners, userAdmins...), nil
}
func convertUsers(users []database.User, organizationIDsByUserID map[uuid.UUID][]uuid.UUID) []codersdk.User {
converted := make([]codersdk.User, 0, len(users))
for _, u := range users {
+84
View File
@@ -374,6 +374,90 @@ func TestDeleteUser(t *testing.T) {
})
}
func TestNotifyDeletedUser(t *testing.T) {
t.Parallel()
t.Run("OwnerNotified", func(t *testing.T) {
t.Parallel()
// given
notifyEnq := &testutil.FakeNotificationsEnqueuer{}
adminClient := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
firstUser := coderdtest.CreateFirstUser(t, adminClient)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
// when
err = adminClient.DeleteUser(context.Background(), user.ID)
require.NoError(t, err)
// then
require.Len(t, notifyEnq.Sent, 2)
// notifyEnq.Sent[0] is create account event
require.Equal(t, notifications.TemplateUserAccountDeleted, notifyEnq.Sent[1].TemplateID)
require.Equal(t, firstUser.UserID, notifyEnq.Sent[1].UserID)
require.Contains(t, notifyEnq.Sent[1].Targets, user.ID)
require.Equal(t, user.Username, notifyEnq.Sent[1].Labels["deleted_account_name"])
})
t.Run("UserAdminNotified", func(t *testing.T) {
t.Parallel()
// given
notifyEnq := &testutil.FakeNotificationsEnqueuer{}
adminClient := coderdtest.New(t, &coderdtest.Options{
NotificationsEnqueuer: notifyEnq,
})
firstUser := coderdtest.CreateFirstUser(t, adminClient)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin())
member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
OrganizationID: firstUser.OrganizationID,
Email: "another@user.org",
Username: "someone-else",
Password: "SomeSecurePassword!",
})
require.NoError(t, err)
// when
err = adminClient.DeleteUser(context.Background(), member.ID)
require.NoError(t, err)
// then
require.Len(t, notifyEnq.Sent, 5)
// notifyEnq.Sent[0]: "User admin" account created, "owner" notified
// notifyEnq.Sent[1]: "Member" account created, "owner" notified
// notifyEnq.Sent[2]: "Member" account created, "user admin" notified
// "Member" account deleted, "owner" notified
require.Equal(t, notifications.TemplateUserAccountDeleted, notifyEnq.Sent[3].TemplateID)
require.Equal(t, firstUser.UserID, notifyEnq.Sent[3].UserID)
require.Contains(t, notifyEnq.Sent[3].Targets, member.ID)
require.Equal(t, member.Username, notifyEnq.Sent[3].Labels["deleted_account_name"])
// "Member" account deleted, "user admin" notified
require.Equal(t, notifications.TemplateUserAccountDeleted, notifyEnq.Sent[4].TemplateID)
require.Equal(t, userAdmin.ID, notifyEnq.Sent[4].UserID)
require.Contains(t, notifyEnq.Sent[4].Targets, member.ID)
require.Equal(t, member.Username, notifyEnq.Sent[4].Labels["deleted_account_name"])
})
}
func TestPostLogout(t *testing.T) {
t.Parallel()