Skip to content

Commit 6428a76

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

File tree

5 files changed

+138
-14
lines changed

5 files changed

+138
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETE FROM notification_templates WHERE id = 'f44d9314-ad03-4bc8-95d0-5cad491da6b6';
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

+1
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

+43-14
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

+84
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)