-
Notifications
You must be signed in to change notification settings - Fork 943
feat: notify when a user account is deleted #14113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
42f4bd7
9c47637
4e787ec
15fec2b
37bbb41
96fa649
ac5a3e6
085c5bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
DELETE FROM notification_templates WHERE id = 'f44d9314-ad03-4bc8-95d0-5cad491da6b6'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -567,6 +567,24 @@ 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 { | ||
api.Logger.Warn(ctx, "unable to fetch user admins", slog.Error(err)) | ||
rw.WriteHeader(http.StatusNoContent) | ||
} | ||
|
||
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 +1305,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}, | ||
}) | ||
if err != nil { | ||
return user, req.OrganizationID, xerrors.Errorf("get owners: %w", err) | ||
} | ||
userAdmins, err := store.GetUsers(ctx, database.GetUsersParams{ | ||
RbacRole: []string{codersdk.RoleUserAdmin}, | ||
}) | ||
userAdmins, err := findUserAdmins(ctx, store) | ||
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 +1323,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{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we also need to include org user admins here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably, I will leave it as a follow-up once the org part is final. Most likely we need to adjust create account logic too. |
||
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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -374,6 +374,103 @@ 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
6D47
Should the owner receive an email for their own action? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can't delete your own account, so I believe there is no need to filter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What i mean is: if an owner initiated an event - they should probably not receive a notification about it because it's redundant. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand that they won't receive as the API method will fail faster? see code There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh.. you mean the owner of the action.. not the owner of the account. Interesting case, on the other hand, if somebody took over the account, notifications would work like auditing. |
||
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, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ | ||
OrganizationID: firstUser.OrganizationID, | ||
Email: "user-admin@user.org", | ||
Username: "mr-user-admin", | ||
Password: "SomeSecurePassword!", | ||
}) | ||
require.NoError(t, err) | ||
|
||
_, err = adminClient.UpdateUserRoles(ctx, userAdmin.Username, codersdk.UpdateRoles{ | ||
Roles: []string{ | ||
rbac.RoleUserAdmin().String(), | ||
}, | ||
}) | ||
mtojek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
require.NoError(t, err) | ||
|
||
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() | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.