Skip to content

Commit 6428a76

Browse files
authored
feat: notify when a user account is deleted (#14113)
1 parent 4242fd9 commit 6428a76

File tree

5 files changed

+138
-14
lines changed

5 files changed

+138
-14
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETE FROM notification_templates WHERE id = 'f44d9314-ad03-4bc8-95d0-5cad491da6b6';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions)
2+
VALUES ('f44d9314-ad03-4bc8-95d0-5cad491da6b6', 'User account deleted', E'User account "{{.Labels.deleted_account_name}}" deleted',
3+
E'Hi {{.UserName}},\n\nUser account **{{.Labels.deleted_account_name}}** has been deleted.',
4+
'User Events', '[
5+
{
6+
"label": "View accounts",
7+
"url": "{{ base_url }}/deployment/users?filter=status%3Aactive"
8+
}
9+
]'::jsonb);

coderd/notifications/events.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ var (
1717
// Account-related events.
1818
var (
1919
TemplateUserAccountCreated = uuid.MustParse("4e19c0ac-94e1-4532-9515-d1801aa283b2")
20+
TemplateUserAccountDeleted = uuid.MustParse("f44d9314-ad03-4bc8-95d0-5cad491da6b6")
2021
)

coderd/users.go

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,27 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) {
567567
}
568568
user.Deleted = true
569569
aReq.New = user
570+
571+
userAdmins, err := findUserAdmins(ctx, api.Database)
572+
if err != nil {
573+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
574+
Message: "Internal error fetching user admins.",
575+
Detail: err.Error(),
576+
})
577+
return
578+
}
579+
580+
for _, u := range userAdmins {
581+
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, notifications.TemplateUserAccountDeleted,
582+
map[string]string{
583+
"deleted_account_name": user.Username,
584+
}, "api-users-delete",
585+
user.ID,
586+
); err != nil {
587+
api.Logger.Warn(ctx, "unable to notify about deleted user", slog.F("deleted_user", user.Username), slog.Error(err))
588+
}
589+
}
590+
570591
rw.WriteHeader(http.StatusNoContent)
571592
}
572593

@@ -1287,23 +1308,12 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
12871308
return user, req.OrganizationID, err
12881309
}
12891310

1290-
// Notify all users with user admin permission including owners
1291-
// Notice: we can't scrape the user information in parallel as pq
1292-
// fails with: unexpected describe rows response: 'D'
1293-
owners, err := store.GetUsers(ctx, database.GetUsersParams{
1294-
RbacRole: []string{codersdk.RoleOwner},
1295-
})
1311+
userAdmins, err := findUserAdmins(ctx, store)
12961312
if err != nil {
1297-
return user, req.OrganizationID, xerrors.Errorf("get owners: %w", err)
1298-
}
1299-
userAdmins, err := store.GetUsers(ctx, database.GetUsersParams{
1300-
RbacRole: []string{codersdk.RoleUserAdmin},
1301-
})
1302-
if err != nil {
1303-
return user, req.OrganizationID, xerrors.Errorf("get user admins: %w", err)
1313+
return user, req.OrganizationID, xerrors.Errorf("find user admins: %w", err)
13041314
}
13051315

1306-
for _, u := range append(owners, userAdmins...) {
1316+
for _, u := range userAdmins {
13071317
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, notifications.TemplateUserAccountCreated,
13081318
map[string]string{
13091319
"created_account_name": user.Username,
@@ -1316,6 +1326,25 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
13161326
return user, req.OrganizationID, err
13171327
}
13181328

1329+
// findUserAdmins fetches all users with user admin permission including owners.
1330+
func findUserAdmins(ctx context.Context, store database.Store) ([]database.GetUsersRow, error) {
1331+
// Notice: we can't scrape the user information in parallel as pq
1332+
// fails with: unexpected describe rows response: 'D'
1333+
owners, err := store.GetUsers(ctx, database.GetUsersParams{
1334+
RbacRole: []string{codersdk.RoleOwner},
1335+
})
1336+
if err != nil {
1337+
return nil, xerrors.Errorf("get owners: %w", err)
1338+
}
1339+
userAdmins, err := store.GetUsers(ctx, database.GetUsersParams{
1340+
RbacRole: []string{codersdk.RoleUserAdmin},
1341+
})
1342+
if err != nil {
1343+
return nil, xerrors.Errorf("get user admins: %w", err)
1344+
}
1345+
return append(owners, userAdmins...), nil
1346+
}
1347+
13191348
func convertUsers(users []database.User, organizationIDsByUserID map[uuid.UUID][]uuid.UUID) []codersdk.User {
13201349
converted := make([]codersdk.User, 0, len(users))
13211350
for _, u := range users {

coderd/users_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,90 @@ func TestDeleteUser(t *testing.T) {
374374
})
375375
}
376376

377+
func TestNotifyDeletedUser(t *testing.T) {
378+
t.Parallel()
379+
380+
t.Run("OwnerNotified", func(t *testing.T) {
381+
t.Parallel()
382+
383+
// given
384+
notifyEnq := &testutil.FakeNotificationsEnqueuer{}
385+
adminClient := coderdtest.New(t, &coderdtest.Options{
386+
NotificationsEnqueuer: notifyEnq,
387+
})
388+
firstUser := coderdtest.CreateFirstUser(t, adminClient)
389+
390+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
391+
defer cancel()
392+
393+
user, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
394+
OrganizationID: firstUser.OrganizationID,
395+
Email: "another@user.org",
396+
Username: "someone-else",
397+
Password: "SomeSecurePassword!",
398+
})
399+
require.NoError(t, err)
400+
401+
// when
402+
err = adminClient.DeleteUser(context.Background(), user.ID)
403+
require.NoError(t, err)
404+
405+
// then
406+
require.Len(t, notifyEnq.Sent, 2)
407+
// notifyEnq.Sent[0] is create account event
408+
require.Equal(t, notifications.TemplateUserAccountDeleted, notifyEnq.Sent[1].TemplateID)
409+
require.Equal(t, firstUser.UserID, notifyEnq.Sent[1].UserID)
410+
require.Contains(t, notifyEnq.Sent[1].Targets, user.ID)
411+
require.Equal(t, user.Username, notifyEnq.Sent[1].Labels["deleted_account_name"])
412+
})
413+
414+
t.Run("UserAdminNotified", func(t *testing.T) {
415+
t.Parallel()
416+
417+
// given
418+
notifyEnq := &testutil.FakeNotificationsEnqueuer{}
419+
adminClient := coderdtest.New(t, &coderdtest.Options{
420+
NotificationsEnqueuer: notifyEnq,
421+
})
422+
firstUser := coderdtest.CreateFirstUser(t, adminClient)
423+
424+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
425+
defer cancel()
426+
427+
_, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin())
428+
429+
member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
430+
OrganizationID: firstUser.OrganizationID,
431+
Email: "another@user.org",
432+
Username: "someone-else",
433+
Password: "SomeSecurePassword!",
434+
})
435+
require.NoError(t, err)
436+
437+
// when
438+
err = adminClient.DeleteUser(context.Background(), member.ID)
439+
require.NoError(t, err)
440+
441+
// then
442+
require.Len(t, notifyEnq.Sent, 5)
443+
// notifyEnq.Sent[0]: "User admin" account created, "owner" notified
444+
// notifyEnq.Sent[1]: "Member" account created, "owner" notified
445+
// notifyEnq.Sent[2]: "Member" account created, "user admin" notified
446+
447+
// "Member" account deleted, "owner" notified
448+
require.Equal(t, notifications.TemplateUserAccountDeleted, notifyEnq.Sent[3].TemplateID)
449+
require.Equal(t, firstUser.UserID, notifyEnq.Sent[3].UserID)
450+
require.Contains(t, notifyEnq.Sent[3].Targets, member.ID)
451+
require.Equal(t, member.Username, notifyEnq.Sent[3].Labels["deleted_account_name"])
452+
453+
// "Member" account deleted, "user admin" notified
454+
require.Equal(t, notifications.TemplateUserAccountDeleted, notifyEnq.Sent[4].TemplateID)
455+
require.Equal(t, userAdmin.ID, notifyEnq.Sent[4].UserID)
456+
require.Contains(t, notifyEnq.Sent[4].Targets, member.ID)
457+
require.Equal(t, member.Username, notifyEnq.Sent[4].Labels["deleted_account_name"])
458+
})
459+
}
460+
377461
func TestPostLogout(t *testing.T) {
378462
t.Parallel()
379463

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy