mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: notify when a user account is deleted (#14113)
This commit is contained in:
@@ -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);
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user