From 000a91778a0f570c446484ca0ddcf43d0898adeb Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 20 Aug 2024 15:29:20 +0200 Subject: [PATCH 01/18] migrations --- ...00245_notifications_user_suspended.down.sql | 2 ++ .../000245_notifications_user_suspended.up.sql | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 coderd/database/migrations/000245_notifications_user_suspended.down.sql create mode 100644 coderd/database/migrations/000245_notifications_user_suspended.up.sql diff --git a/coderd/database/migrations/000245_notifications_user_suspended.down.sql b/coderd/database/migrations/000245_notifications_user_suspended.down.sql new file mode 100644 index 0000000000000..e75bf5a754a4a --- /dev/null +++ b/coderd/database/migrations/000245_notifications_user_suspended.down.sql @@ -0,0 +1,2 @@ +DELETE FROM notification_templates WHERE id = 'b02ddd82-4733-4d02-a2d7-c36f3598997d'; +DELETE FROM notification_templates WHERE id = '9f5af851-8408-4e73-a7a1-c6502ba46689'; diff --git a/coderd/database/migrations/000245_notifications_user_suspended.up.sql b/coderd/database/migrations/000245_notifications_user_suspended.up.sql new file mode 100644 index 0000000000000..dbeb6bd03fa9e --- /dev/null +++ b/coderd/database/migrations/000245_notifications_user_suspended.up.sql @@ -0,0 +1,18 @@ +INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) +VALUES ('b02ddd82-4733-4d02-a2d7-c36f3598997d', 'User account suspended', E'User account "{{.Labels.suspended_account_name}}" suspended', + E'Hi {{.UserName}},\n\User account **{{.Labels.suspended_account_name}}** has been suspended.', + 'Workspace Events', '[ + { + "label": "View accounts", + "url": "{{ base_url }}/deployment/users?filter=status%3Aactive" + } + ]'::jsonb); +INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) +VALUES ('9f5af851-8408-4e73-a7a1-c6502ba46689', 'User account reactivated', E'User account "{{.Labels.reactivated_account_name}}" reactivated', + E'Hi {{.UserName}},\n\User account **{{.Labels.reactivated_account_name}}** has been reactivated.', + 'Workspace Events', '[ + { + "label": "View accounts", + "url": "{{ base_url }}/deployment/users?filter=status%3Aactive" + } + ]'::jsonb); From 62cf15fd6c69b4002e73ffc59c2929b13f053706 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 20 Aug 2024 17:01:11 +0200 Subject: [PATCH 02/18] notify --- ...000245_notifications_user_suspended.up.sql | 4 +- coderd/notifications/events.go | 3 + coderd/users.go | 60 +++++++++++++++++-- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/coderd/database/migrations/000245_notifications_user_suspended.up.sql b/coderd/database/migrations/000245_notifications_user_suspended.up.sql index dbeb6bd03fa9e..451ef7982cff4 100644 --- a/coderd/database/migrations/000245_notifications_user_suspended.up.sql +++ b/coderd/database/migrations/000245_notifications_user_suspended.up.sql @@ -8,8 +8,8 @@ VALUES ('b02ddd82-4733-4d02-a2d7-c36f3598997d', 'User account suspended', E'User } ]'::jsonb); INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) -VALUES ('9f5af851-8408-4e73-a7a1-c6502ba46689', 'User account reactivated', E'User account "{{.Labels.reactivated_account_name}}" reactivated', - E'Hi {{.UserName}},\n\User account **{{.Labels.reactivated_account_name}}** has been reactivated.', +VALUES ('9f5af851-8408-4e73-a7a1-c6502ba46689', 'User account activated', E'User account "{{.Labels.activated_account_name}}" activated', + E'Hi {{.UserName}},\n\User account **{{.Labels.activated_account_name}}** has been activated.', 'Workspace Events', '[ { "label": "View accounts", diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index b340b281e0757..0c5c0ebbf6ad1 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -18,6 +18,9 @@ var ( var ( TemplateUserAccountCreated = uuid.MustParse("4e19c0ac-94e1-4532-9515-d1801aa283b2") TemplateUserAccountDeleted = uuid.MustParse("f44d9314-ad03-4bc8-95d0-5cad491da6b6") + + TemplateUserAccountSuspended = uuid.MustParse("b02ddd82-4733-4d02-a2d7-c36f3598997d") + TemplateUserAccountActivated = uuid.MustParse("9f5af851-8408-4e73-a7a1-c6502ba46689") ) // Template-related events. diff --git a/coderd/users.go b/coderd/users.go index cde7271ca4e5d..2f1ae2f837f89 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -845,7 +845,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } } - suspendedUser, err := api.Database.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ + updatedUser, err := api.Database.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ ID: user.ID, Status: status, UpdatedAt: dbtime.Now(), @@ -857,8 +857,61 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW }) return } - aReq.New = suspendedUser + aReq.New = updatedUser + + // Notify about the change of user status + var key string + var templateID uuid.UUID + switch status { + case database.UserStatusSuspended: + key = "suspended_account_name" + templateID = notifications.TemplateUserAccountSuspended + case database.UserStatusActive: + key = "activated_account_name" + templateID = notifications.TemplateUserAccountSuspended + default: + api.Logger.Error(ctx, "unable to notify admins as the user status is unsupported", slog.F("username", user.Username), slog.F("user_status", string(status))) + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error preparing notifications", + }) + } + + // Fetch all users with user admin permissions + owners, err := api.Database.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleOwner}, + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching owners", + Detail: err.Error(), + }) + return + } + userAdmins, err := api.Database.GetUsers(ctx, database.GetUsersParams{ + RbacRole: []string{codersdk.RoleUserAdmin}, + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching user admins", + Detail: err.Error(), + }) + return + } + + // Send notifications to user admins and affected user + for _, u := range append(append(owners, userAdmins...), database.GetUsersRow{ID: user.ID}) { + if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, templateID, + map[string]string{ + key: user.Username, + }, "api-put-user-status", + user.ID, + ); err != nil { + api.Logger.Warn(ctx, "unable to notify about changed user status", slog.F("affected_user", user.Username), slog.Error(err)) + } + } + + // Finish: build final response organizations, err := userOrganizationIDs(ctx, api, user) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -867,8 +920,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW }) return } - - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(suspendedUser, organizations)) + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUser, organizations)) } } From 323680c419445fd562d1d31ea6003d193aeabe08 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 20 Aug 2024 17:03:50 +0200 Subject: [PATCH 03/18] fix --- coderd/users.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/users.go b/coderd/users.go index 2f1ae2f837f89..595de375bac53 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -845,7 +845,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } } - updatedUser, err := api.Database.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ + targetUser, err := api.Database.UpdateUserStatus(ctx, database.UpdateUserStatusParams{ ID: user.ID, Status: status, UpdatedAt: dbtime.Now(), @@ -857,7 +857,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW }) return } - aReq.New = updatedUser + aReq.New = targetUser // Notify about the change of user status var key string @@ -870,7 +870,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW key = "activated_account_name" templateID = notifications.TemplateUserAccountSuspended default: - api.Logger.Error(ctx, "unable to notify admins as the user status is unsupported", slog.F("username", user.Username), slog.F("user_status", string(status))) + api.Logger.Error(ctx, "unable to notify admins as the user's status is unsupported", slog.F("username", user.Username), slog.F("user_status", string(status))) httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error preparing notifications", @@ -907,7 +907,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW }, "api-put-user-status", user.ID, ); err != nil { - api.Logger.Warn(ctx, "unable to notify about changed user status", slog.F("affected_user", user.Username), slog.Error(err)) + api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err)) } } @@ -920,7 +920,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW }) return } - httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(updatedUser, organizations)) + httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(targetUser, organizations)) } } From bbfb2964fdd9c28e8a9e21e40b2235603a42abec Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 21 Aug 2024 13:37:18 +0200 Subject: [PATCH 04/18] TestNotifyUserSuspended --- coderd/users_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/coderd/users_test.go b/coderd/users_test.go index 4f44da42ed59b..af14e159b8a03 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -374,6 +374,58 @@ func TestDeleteUser(t *testing.T) { }) } +func TestNotifyUserSuspended(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.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended) + require.NoError(t, err) + + // then + require.Len(t, notifyEnq.Sent, 6) + // 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 suspended, "owner" notified + require.Equal(t, notifications.TemplateUserAccountSuspended, 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["suspended_account_name"]) + + // "Member" account suspended, "user admin" notified + require.Equal(t, notifications.TemplateUserAccountSuspended, 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["suspended_account_name"]) + + // "Member" account suspended, "member" notified + require.Equal(t, notifications.TemplateUserAccountSuspended, notifyEnq.Sent[5].TemplateID) + require.Equal(t, member.ID, notifyEnq.Sent[5].UserID) + require.Contains(t, notifyEnq.Sent[5].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[5].Labels["suspended_account_name"]) +} + func TestNotifyDeletedUser(t *testing.T) { t.Parallel() From e1b6c289c0cbb5e8c86b679528f80a282c88d5a5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 21 Aug 2024 13:47:01 +0200 Subject: [PATCH 05/18] TestNotifyUserReactivate --- coderd/users.go | 2 +- coderd/users_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/coderd/users.go b/coderd/users.go index 595de375bac53..df4ab099a8681 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -868,7 +868,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW templateID = notifications.TemplateUserAccountSuspended case database.UserStatusActive: key = "activated_account_name" - templateID = notifications.TemplateUserAccountSuspended + templateID = notifications.TemplateUserAccountActivated default: api.Logger.Error(ctx, "unable to notify admins as the user's status is unsupported", slog.F("username", user.Username), slog.F("user_status", string(status))) diff --git a/coderd/users_test.go b/coderd/users_test.go index af14e159b8a03..2684f8bacfd3b 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -402,7 +402,7 @@ func TestNotifyUserSuspended(t *testing.T) { require.NoError(t, err) // then - require.Len(t, notifyEnq.Sent, 6) + require.Len(t, notifyEnq.Sent, 3+3) // 3 notifications due to acccount creation + 3 extra due to account suspension // 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 @@ -426,6 +426,61 @@ func TestNotifyUserSuspended(t *testing.T) { require.Equal(t, member.Username, notifyEnq.Sent[5].Labels["suspended_account_name"]) } +func TestNotifyUserReactivate(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) + + _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended) + require.NoError(t, err) + + // when + _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusActive) + require.NoError(t, err) + + // then + require.Len(t, notifyEnq.Sent, 6+3) // 6 notifications due to account suspension + 3 extra due to activation + // 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 suspended, "owner" notified + require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[6].TemplateID) + require.Equal(t, firstUser.UserID, notifyEnq.Sent[6].UserID) + require.Contains(t, notifyEnq.Sent[6].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[6].Labels["activated_account_name"]) + + // "Member" account suspended, "user admin" notified + require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[7].TemplateID) + require.Equal(t, userAdmin.ID, notifyEnq.Sent[7].UserID) + require.Contains(t, notifyEnq.Sent[7].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[7].Labels["activated_account_name"]) + + // "Member" account suspended, "member" notified + require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[8].TemplateID) + require.Equal(t, member.ID, notifyEnq.Sent[8].UserID) + require.Contains(t, notifyEnq.Sent[8].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[8].Labels["activated_account_name"]) +} + func TestNotifyDeletedUser(t *testing.T) { t.Parallel() From c7783ac061ad88ff66d29d8896f58cd599e5de79 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 21 Aug 2024 13:51:29 +0200 Subject: [PATCH 06/18] post merge --- ...nded.down.sql => 000246_notifications_user_suspended.down.sql} | 0 ...uspended.up.sql => 000246_notifications_user_suspended.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000245_notifications_user_suspended.down.sql => 000246_notifications_user_suspended.down.sql} (100%) rename coderd/database/migrations/{000245_notifications_user_suspended.up.sql => 000246_notifications_user_suspended.up.sql} (100%) diff --git a/coderd/database/migrations/000245_notifications_user_suspended.down.sql b/coderd/database/migrations/000246_notifications_user_suspended.down.sql similarity index 100% rename from coderd/database/migrations/000245_notifications_user_suspended.down.sql rename to coderd/database/migrations/000246_notifications_user_suspended.down.sql diff --git a/coderd/database/migrations/000245_notifications_user_suspended.up.sql b/coderd/database/migrations/000246_notifications_user_suspended.up.sql similarity index 100% rename from coderd/database/migrations/000245_notifications_user_suspended.up.sql rename to coderd/database/migrations/000246_notifications_user_suspended.up.sql From 55e4e13c510885c3aeb38bcee008293b0e587ced Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 21 Aug 2024 14:03:06 +0200 Subject: [PATCH 07/18] fix escape --- .../migrations/000246_notifications_user_suspended.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000246_notifications_user_suspended.up.sql b/coderd/database/migrations/000246_notifications_user_suspended.up.sql index 451ef7982cff4..e91fd55d34a5e 100644 --- a/coderd/database/migrations/000246_notifications_user_suspended.up.sql +++ b/coderd/database/migrations/000246_notifications_user_suspended.up.sql @@ -1,6 +1,6 @@ INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('b02ddd82-4733-4d02-a2d7-c36f3598997d', 'User account suspended', E'User account "{{.Labels.suspended_account_name}}" suspended', - E'Hi {{.UserName}},\n\User account **{{.Labels.suspended_account_name}}** has been suspended.', + E'Hi {{.UserName}},\nUser account **{{.Labels.suspended_account_name}}** has been suspended.', 'Workspace Events', '[ { "label": "View accounts", @@ -9,7 +9,7 @@ VALUES ('b02ddd82-4733-4d02-a2d7-c36f3598997d', 'User account suspended', E'User ]'::jsonb); INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('9f5af851-8408-4e73-a7a1-c6502ba46689', 'User account activated', E'User account "{{.Labels.activated_account_name}}" activated', - E'Hi {{.UserName}},\n\User account **{{.Labels.activated_account_name}}** has been activated.', + E'Hi {{.UserName}},\nUser account **{{.Labels.activated_account_name}}** has been activated.', 'Workspace Events', '[ { "label": "View accounts", From 38bca289cd93cf5dbd8baa881ca1e54a62e4932a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 21 Aug 2024 14:12:08 +0200 Subject: [PATCH 08/18] TestNotificationTemplatesCanRender --- coderd/notifications/notifications_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 8ecae8a904923..a7cf315340b44 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -756,6 +756,26 @@ func TestNotificationTemplatesCanRender(t *testing.T) { }, }, }, + { + name: "TemplateUserAccountSuspended", + id: notifications.TemplateUserAccountSuspended, + payload: types.MessagePayload{ + UserName: "bobby", + Labels: map[string]string{ + "suspended_account_name": "bobby", + }, + }, + }, + { + name: "TemplateUserAccountActivated", + id: notifications.TemplateUserAccountActivated, + payload: types.MessagePayload{ + UserName: "bobby", + Labels: map[string]string{ + "activated_account_name": "bobby", + }, + }, + }, { name: "TemplateTemplateDeleted", id: notifications.TemplateTemplateDeleted, From 37add7d01e9da75c0b9b80766b9acef06ac2e6f5 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 11:28:53 +0200 Subject: [PATCH 09/18] links and events --- .../migrations/000246_notifications_user_suspended.up.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/database/migrations/000246_notifications_user_suspended.up.sql b/coderd/database/migrations/000246_notifications_user_suspended.up.sql index e91fd55d34a5e..f41967af7095b 100644 --- a/coderd/database/migrations/000246_notifications_user_suspended.up.sql +++ b/coderd/database/migrations/000246_notifications_user_suspended.up.sql @@ -1,16 +1,16 @@ INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('b02ddd82-4733-4d02-a2d7-c36f3598997d', 'User account suspended', E'User account "{{.Labels.suspended_account_name}}" suspended', E'Hi {{.UserName}},\nUser account **{{.Labels.suspended_account_name}}** has been suspended.', - 'Workspace Events', '[ + 'User Events', '[ { - "label": "View accounts", - "url": "{{ base_url }}/deployment/users?filter=status%3Aactive" + "label": "View suspended accounts", + "url": "{{ base_url }}/deployment/users?filter=status%3Asuspended" } ]'::jsonb); INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('9f5af851-8408-4e73-a7a1-c6502ba46689', 'User account activated', E'User account "{{.Labels.activated_account_name}}" activated', E'Hi {{.UserName}},\nUser account **{{.Labels.activated_account_name}}** has been activated.', - 'Workspace Events', '[ + 'User Events', '[ { "label": "View accounts", "url": "{{ base_url }}/deployment/users?filter=status%3Aactive" From baf1a954d29d03d0851c9f0cbd92369cc20fa5f9 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 11:32:10 +0200 Subject: [PATCH 10/18] notifyEnq --- coderd/users_test.go | 62 +++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index 2684f8bacfd3b..2d1736dbeb339 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -397,33 +397,32 @@ func TestNotifyUserSuspended(t *testing.T) { }) require.NoError(t, err) + notifyEnq.Clear() + // when _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended) require.NoError(t, err) // then - require.Len(t, notifyEnq.Sent, 3+3) // 3 notifications due to acccount creation + 3 extra due to account suspension - // 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 + require.Len(t, notifyEnq.Sent, 3) // "Member" account suspended, "owner" notified - require.Equal(t, notifications.TemplateUserAccountSuspended, 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["suspended_account_name"]) + require.Equal(t, notifications.TemplateUserAccountSuspended, notifyEnq.Sent[0].TemplateID) + require.Equal(t, firstUser.UserID, notifyEnq.Sent[0].UserID) + require.Contains(t, notifyEnq.Sent[0].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[0].Labels["suspended_account_name"]) // "Member" account suspended, "user admin" notified - require.Equal(t, notifications.TemplateUserAccountSuspended, 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["suspended_account_name"]) + require.Equal(t, notifications.TemplateUserAccountSuspended, notifyEnq.Sent[1].TemplateID) + require.Equal(t, userAdmin.ID, notifyEnq.Sent[1].UserID) + require.Contains(t, notifyEnq.Sent[1].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[1].Labels["suspended_account_name"]) // "Member" account suspended, "member" notified - require.Equal(t, notifications.TemplateUserAccountSuspended, notifyEnq.Sent[5].TemplateID) - require.Equal(t, member.ID, notifyEnq.Sent[5].UserID) - require.Contains(t, notifyEnq.Sent[5].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[5].Labels["suspended_account_name"]) + require.Equal(t, notifications.TemplateUserAccountSuspended, notifyEnq.Sent[2].TemplateID) + require.Equal(t, member.ID, notifyEnq.Sent[2].UserID) + require.Contains(t, notifyEnq.Sent[2].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[2].Labels["suspended_account_name"]) } func TestNotifyUserReactivate(t *testing.T) { @@ -452,33 +451,32 @@ func TestNotifyUserReactivate(t *testing.T) { _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended) require.NoError(t, err) + notifyEnq.Clear() + // when _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusActive) require.NoError(t, err) // then - require.Len(t, notifyEnq.Sent, 6+3) // 6 notifications due to account suspension + 3 extra due to activation - // 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 + require.Len(t, notifyEnq.Sent, 3) // "Member" account suspended, "owner" notified - require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[6].TemplateID) - require.Equal(t, firstUser.UserID, notifyEnq.Sent[6].UserID) - require.Contains(t, notifyEnq.Sent[6].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[6].Labels["activated_account_name"]) + require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[0].TemplateID) + require.Equal(t, firstUser.UserID, notifyEnq.Sent[0].UserID) + require.Contains(t, notifyEnq.Sent[0].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[0].Labels["activated_account_name"]) // "Member" account suspended, "user admin" notified - require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[7].TemplateID) - require.Equal(t, userAdmin.ID, notifyEnq.Sent[7].UserID) - require.Contains(t, notifyEnq.Sent[7].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[7].Labels["activated_account_name"]) + require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[1].TemplateID) + require.Equal(t, userAdmin.ID, notifyEnq.Sent[1].UserID) + require.Contains(t, notifyEnq.Sent[1].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[1].Labels["activated_account_name"]) // "Member" account suspended, "member" notified - require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[8].TemplateID) - require.Equal(t, member.ID, notifyEnq.Sent[8].UserID) - require.Contains(t, notifyEnq.Sent[8].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[8].Labels["activated_account_name"]) + require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[2].TemplateID) + require.Equal(t, member.ID, notifyEnq.Sent[2].UserID) + require.Contains(t, notifyEnq.Sent[2].Targets, member.ID) + require.Equal(t, member.Username, notifyEnq.Sent[2].Labels["activated_account_name"]) } func TestNotifyDeletedUser(t *testing.T) { From a6744764becfab24cf37a3e94a086f8efdf272b6 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 11:42:22 +0200 Subject: [PATCH 11/18] findUserAdmins --- coderd/users.go | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/coderd/users.go b/coderd/users.go index df4ab099a8681..311f6e4adbe29 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -878,19 +878,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } // Fetch all users with user admin permissions - owners, err := api.Database.GetUsers(ctx, database.GetUsersParams{ - RbacRole: []string{codersdk.RoleOwner}, - }) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching owners", - Detail: err.Error(), - }) - return - } - userAdmins, err := api.Database.GetUsers(ctx, database.GetUsersParams{ - RbacRole: []string{codersdk.RoleUserAdmin}, - }) + userAdmins, err := findUserAdmins(ctx, api.Database) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error fetching user admins", @@ -900,7 +888,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } // Send notifications to user admins and affected user - for _, u := range append(append(owners, userAdmins...), database.GetUsersRow{ID: user.ID}) { + for _, u := range append(userAdmins, database.GetUsersRow{ID: user.ID}) { if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, templateID, map[string]string{ key: user.Username, From 96611b1f630c0c0fc5df3df28fbdf82071ae5a3b Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 11:51:52 +0200 Subject: [PATCH 12/18] notifyUserStatusChanged --- coderd/users.go | 72 ++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/coderd/users.go b/coderd/users.go index 311f6e4adbe29..a9672ead2435d 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -859,47 +859,15 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } aReq.New = targetUser - // Notify about the change of user status - var key string - var templateID uuid.UUID - switch status { - case database.UserStatusSuspended: - key = "suspended_account_name" - templateID = notifications.TemplateUserAccountSuspended - case database.UserStatusActive: - key = "activated_account_name" - templateID = notifications.TemplateUserAccountActivated - default: - api.Logger.Error(ctx, "unable to notify admins as the user's status is unsupported", slog.F("username", user.Username), slog.F("user_status", string(status))) - - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error preparing notifications", - }) - } - - // Fetch all users with user admin permissions - userAdmins, err := findUserAdmins(ctx, api.Database) + err = api.notifyUserStatusChanged() if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching user admins", + Message: "Internal error notifying about changed user status.", Detail: err.Error(), }) return } - // Send notifications to user admins and affected user - for _, u := range append(userAdmins, database.GetUsersRow{ID: user.ID}) { - if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, templateID, - map[string]string{ - key: user.Username, - }, "api-put-user-status", - user.ID, - ); err != nil { - api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err)) - } - } - - // Finish: build final response organizations, err := userOrganizationIDs(ctx, api, user) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -912,6 +880,42 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } } +func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, status database.UserStatus) error { + // Notify about the change of user status + var key string + var templateID uuid.UUID + switch status { + case database.UserStatusSuspended: + key = "suspended_account_name" + templateID = notifications.TemplateUserAccountSuspended + case database.UserStatusActive: + key = "activated_account_name" + templateID = notifications.TemplateUserAccountActivated + default: + api.Logger.Error(ctx, "user status is not supported", slog.F("username", user.Username), slog.F("user_status", string(status))) + return xerrors.Errorf("unable to notify admins as the user's status is unsupported") + } + + // Fetch all users with user admin permissions + userAdmins, err := findUserAdmins(ctx, api.Database) + if err != nil { + return xerrors.Errorf("unable to find user admins: %w", err) + } + + // Send notifications to user admins and affected user + for _, u := range append(userAdmins, database.GetUsersRow{ID: user.ID}) { + if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, templateID, + map[string]string{ + key: user.Username, + }, "api-put-user-status", + user.ID, + ); err != nil { + api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err)) + } + } + return nil +} + // @Summary Update user appearance settings // @ID update-user-appearance-settings // @Security CoderSessionToken From fabba3c1ee28d9d4c84070482fa5d23efec662f4 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 12:10:54 +0200 Subject: [PATCH 13/18] go build --- coderd/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/users.go b/coderd/users.go index a9672ead2435d..8ab97f7ef6147 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -859,7 +859,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } aReq.New = targetUser - err = api.notifyUserStatusChanged() + err = api.notifyUserStatusChanged(ctx, user, status) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error notifying about changed user status.", From e72bf2a147e880a35bcd3231903c37d211032293 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 12:29:20 +0200 Subject: [PATCH 14/18] your and admin --- ...0246_notifications_user_suspended.down.sql | 2 ++ ...000246_notifications_user_suspended.up.sql | 13 +++++++++++ coderd/notifications/events.go | 2 ++ coderd/users.go | 22 +++++++++++++------ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/coderd/database/migrations/000246_notifications_user_suspended.down.sql b/coderd/database/migrations/000246_notifications_user_suspended.down.sql index e75bf5a754a4a..872638e40773d 100644 --- a/coderd/database/migrations/000246_notifications_user_suspended.down.sql +++ b/coderd/database/migrations/000246_notifications_user_suspended.down.sql @@ -1,2 +1,4 @@ DELETE FROM notification_templates WHERE id = 'b02ddd82-4733-4d02-a2d7-c36f3598997d'; +DELETE FROM notification_templates WHERE id = '6a2f0609-9b69-4d36-a989-9f5925b6cbff'; DELETE FROM notification_templates WHERE id = '9f5af851-8408-4e73-a7a1-c6502ba46689'; +DELETE FROM notification_templates WHERE id = '1a6a6bea-ee0a-43e2-9e7c-eabdb53730e4'; diff --git a/coderd/database/migrations/000246_notifications_user_suspended.up.sql b/coderd/database/migrations/000246_notifications_user_suspended.up.sql index f41967af7095b..4ad91db8bfbd8 100644 --- a/coderd/database/migrations/000246_notifications_user_suspended.up.sql +++ b/coderd/database/migrations/000246_notifications_user_suspended.up.sql @@ -8,6 +8,10 @@ VALUES ('b02ddd82-4733-4d02-a2d7-c36f3598997d', 'User account suspended', E'User } ]'::jsonb); INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) +VALUES ('6a2f0609-9b69-4d36-a989-9f5925b6cbff', 'Your account has been suspended', E'Your account "{{.Labels.suspended_account_name}}" has been suspended', + E'Hi {{.UserName}},\nYour account **{{.Labels.suspended_account_name}}** has been suspended.', + 'User Events', '[]'::jsonb); +INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) VALUES ('9f5af851-8408-4e73-a7a1-c6502ba46689', 'User account activated', E'User account "{{.Labels.activated_account_name}}" activated', E'Hi {{.UserName}},\nUser account **{{.Labels.activated_account_name}}** has been activated.', 'User Events', '[ @@ -16,3 +20,12 @@ VALUES ('9f5af851-8408-4e73-a7a1-c6502ba46689', 'User account activated', E'User "url": "{{ base_url }}/deployment/users?filter=status%3Aactive" } ]'::jsonb); +INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions) +VALUES ('1a6a6bea-ee0a-43e2-9e7c-eabdb53730e4', 'Your account has been activated', E'Your account "{{.Labels.activated_account_name}}" has been activated', + E'Hi {{.UserName}},\nYour account **{{.Labels.activated_account_name}}** has been activated.', + 'User Events', '[ + { + "label": "Open Coder", + "url": "{{ base_url }}" + } + ]'::jsonb); diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 0c5c0ebbf6ad1..ee143465bfe6b 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -21,6 +21,8 @@ var ( TemplateUserAccountSuspended = uuid.MustParse("b02ddd82-4733-4d02-a2d7-c36f3598997d") TemplateUserAccountActivated = uuid.MustParse("9f5af851-8408-4e73-a7a1-c6502ba46689") + TemplateYourAccountSuspended = uuid.MustParse("6a2f0609-9b69-4d36-a989-9f5925b6cbff") + TemplateYourAccountActivated = uuid.MustParse("1a6a6bea-ee0a-43e2-9e7c-eabdb53730e4") ) // Template-related events. diff --git a/coderd/users.go b/coderd/users.go index 8ab97f7ef6147..d676ed28ead81 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -881,30 +881,30 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW } func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, status database.UserStatus) error { - // Notify about the change of user status var key string - var templateID uuid.UUID + var adminTemplateID, ownerTemplateID uuid.UUID switch status { case database.UserStatusSuspended: key = "suspended_account_name" - templateID = notifications.TemplateUserAccountSuspended + adminTemplateID = notifications.TemplateUserAccountSuspended + ownerTemplateID = notifications.TemplateYourAccountSuspended case database.UserStatusActive: key = "activated_account_name" - templateID = notifications.TemplateUserAccountActivated + adminTemplateID = notifications.TemplateUserAccountActivated + ownerTemplateID = notifications.TemplateYourAccountActivated default: api.Logger.Error(ctx, "user status is not supported", slog.F("username", user.Username), slog.F("user_status", string(status))) return xerrors.Errorf("unable to notify admins as the user's status is unsupported") } - // Fetch all users with user admin permissions userAdmins, err := findUserAdmins(ctx, api.Database) if err != nil { return xerrors.Errorf("unable to find user admins: %w", err) } // Send notifications to user admins and affected user - for _, u := range append(userAdmins, database.GetUsersRow{ID: user.ID}) { - if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, templateID, + for _, u := range userAdmins { + if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, adminTemplateID, map[string]string{ key: user.Username, }, "api-put-user-status", @@ -913,6 +913,14 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err)) } } + if _, err := api.NotificationsEnqueuer.Enqueue(ctx, user.ID, ownerTemplateID, + map[string]string{ + key: user.Username, + }, "api-put-user-status", + user.ID, + ); err != nil { + api.Logger.Warn(ctx, "unable to notify user about status change of their account", slog.F("affected_user", user.Username), slog.Error(err)) + } return nil } From 80b227a356ef64eaed615d4ddc757a18613810da Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 12:32:02 +0200 Subject: [PATCH 15/18] tests --- coderd/notifications/notifications_test.go | 20 ++++++++++++++++++++ coderd/users_test.go | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index a7cf315340b44..20487e8ffe2f4 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -776,6 +776,26 @@ func TestNotificationTemplatesCanRender(t *testing.T) { }, }, }, + { + name: "TemplateYourAccountSuspended", + id: notifications.TemplateYourAccountSuspended, + payload: types.MessagePayload{ + UserName: "bobby", + Labels: map[string]string{ + "suspended_account_name": "bobby", + }, + }, + }, + { + name: "TemplateYourAccountActivated", + id: notifications.TemplateYourAccountActivated, + payload: types.MessagePayload{ + UserName: "bobby", + Labels: map[string]string{ + "activated_account_name": "bobby", + }, + }, + }, { name: "TemplateTemplateDeleted", id: notifications.TemplateTemplateDeleted, diff --git a/coderd/users_test.go b/coderd/users_test.go index 2d1736dbeb339..b6532d78c93a2 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -419,7 +419,7 @@ func TestNotifyUserSuspended(t *testing.T) { require.Equal(t, member.Username, notifyEnq.Sent[1].Labels["suspended_account_name"]) // "Member" account suspended, "member" notified - require.Equal(t, notifications.TemplateUserAccountSuspended, notifyEnq.Sent[2].TemplateID) + require.Equal(t, notifications.TemplateYourAccountSuspended, notifyEnq.Sent[2].TemplateID) require.Equal(t, member.ID, notifyEnq.Sent[2].UserID) require.Contains(t, notifyEnq.Sent[2].Targets, member.ID) require.Equal(t, member.Username, notifyEnq.Sent[2].Labels["suspended_account_name"]) @@ -473,7 +473,7 @@ func TestNotifyUserReactivate(t *testing.T) { require.Equal(t, member.Username, notifyEnq.Sent[1].Labels["activated_account_name"]) // "Member" account suspended, "member" notified - require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[2].TemplateID) + require.Equal(t, notifications.TemplateYourAccountActivated, notifyEnq.Sent[2].TemplateID) require.Equal(t, member.ID, notifyEnq.Sent[2].UserID) require.Contains(t, notifyEnq.Sent[2].Targets, member.ID) require.Equal(t, member.Username, notifyEnq.Sent[2].Labels["activated_account_name"]) From d27bc788419de3833672794de5a44c6d59e954ff Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 13:08:31 +0200 Subject: [PATCH 16/18] refactor --- coderd/users_test.go | 169 +++++++++++++++++++++---------------------- 1 file changed, 84 insertions(+), 85 deletions(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index b6532d78c93a2..66eb2f8da1f94 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -374,109 +374,108 @@ func TestDeleteUser(t *testing.T) { }) } -func TestNotifyUserSuspended(t *testing.T) { +func TestNotifyUserStatusChanged(t *testing.T) { t.Parallel() - // given - notifyEnq := &testutil.FakeNotificationsEnqueuer{} - adminClient := coderdtest.New(t, &coderdtest.Options{ - NotificationsEnqueuer: notifyEnq, - }) - firstUser := coderdtest.CreateFirstUser(t, adminClient) + type expectedNotification struct { + TemplateID uuid.UUID + UserID uuid.UUID + } - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + verifyNotificationDispatched := func(notifyEnq *testutil.FakeNotificationsEnqueuer, expectedNotifications []expectedNotification, member codersdk.User, label string) { + require.Equal(t, len(expectedNotifications), len(notifyEnq.Sent)) + + // Validate that each expected notification is present in notifyEnq.Sent + for _, expected := range expectedNotifications { + found := false + for _, sent := range notifyEnq.Sent { + if sent.TemplateID == expected.TemplateID && + sent.UserID == expected.UserID && + slices.Contains(sent.Targets, member.ID) && + sent.Labels[label] == member.Username { + found = true + break + } + } + require.True(t, found, "Expected notification not found: %+v", expected) + } + } - _, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin()) + t.Run("Account suspended", func(t *testing.T) { + t.Parallel() - member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: firstUser.OrganizationID, - Email: "another@user.org", - Username: "someone-else", - Password: "SomeSecurePassword!", - }) - require.NoError(t, err) + notifyEnq := &testutil.FakeNotificationsEnqueuer{} + adminClient := coderdtest.New(t, &coderdtest.Options{ + NotificationsEnqueuer: notifyEnq, + }) + firstUser := coderdtest.CreateFirstUser(t, adminClient) - notifyEnq.Clear() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - // when - _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended) - require.NoError(t, err) + _, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin()) - // then - require.Len(t, notifyEnq.Sent, 3) - - // "Member" account suspended, "owner" notified - require.Equal(t, notifications.TemplateUserAccountSuspended, notifyEnq.Sent[0].TemplateID) - require.Equal(t, firstUser.UserID, notifyEnq.Sent[0].UserID) - require.Contains(t, notifyEnq.Sent[0].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[0].Labels["suspended_account_name"]) - - // "Member" account suspended, "user admin" notified - require.Equal(t, notifications.TemplateUserAccountSuspended, notifyEnq.Sent[1].TemplateID) - require.Equal(t, userAdmin.ID, notifyEnq.Sent[1].UserID) - require.Contains(t, notifyEnq.Sent[1].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[1].Labels["suspended_account_name"]) - - // "Member" account suspended, "member" notified - require.Equal(t, notifications.TemplateYourAccountSuspended, notifyEnq.Sent[2].TemplateID) - require.Equal(t, member.ID, notifyEnq.Sent[2].UserID) - require.Contains(t, notifyEnq.Sent[2].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[2].Labels["suspended_account_name"]) -} + member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ + OrganizationID: firstUser.OrganizationID, + Email: "another@user.org", + Username: "someone-else", + Password: "SomeSecurePassword!", + }) + require.NoError(t, err) -func TestNotifyUserReactivate(t *testing.T) { - t.Parallel() + notifyEnq.Clear() + + // when + _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended) + require.NoError(t, err) - // given - notifyEnq := &testutil.FakeNotificationsEnqueuer{} - adminClient := coderdtest.New(t, &coderdtest.Options{ - NotificationsEnqueuer: notifyEnq, + // then + verifyNotificationDispatched(notifyEnq, []expectedNotification{ + {TemplateID: notifications.TemplateUserAccountSuspended, UserID: firstUser.UserID}, + {TemplateID: notifications.TemplateUserAccountSuspended, UserID: userAdmin.ID}, + {TemplateID: notifications.TemplateYourAccountSuspended, UserID: member.ID}, + }, member, "suspended_account_name") }) - firstUser := coderdtest.CreateFirstUser(t, adminClient) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + t.Run("Account reactivated", func(t *testing.T) { + t.Parallel() - _, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin()) + // given + notifyEnq := &testutil.FakeNotificationsEnqueuer{} + adminClient := coderdtest.New(t, &coderdtest.Options{ + NotificationsEnqueuer: notifyEnq, + }) + firstUser := coderdtest.CreateFirstUser(t, adminClient) - member, err := adminClient.CreateUser(ctx, codersdk.CreateUserRequest{ - OrganizationID: firstUser.OrganizationID, - Email: "another@user.org", - Username: "someone-else", - Password: "SomeSecurePassword!", - }) - require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended) - require.NoError(t, err) + _, userAdmin := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleUserAdmin()) - notifyEnq.Clear() + 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.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusActive) - require.NoError(t, err) + _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusSuspended) + require.NoError(t, err) + + notifyEnq.Clear() - // then - require.Len(t, notifyEnq.Sent, 3) - - // "Member" account suspended, "owner" notified - require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[0].TemplateID) - require.Equal(t, firstUser.UserID, notifyEnq.Sent[0].UserID) - require.Contains(t, notifyEnq.Sent[0].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[0].Labels["activated_account_name"]) - - // "Member" account suspended, "user admin" notified - require.Equal(t, notifications.TemplateUserAccountActivated, notifyEnq.Sent[1].TemplateID) - require.Equal(t, userAdmin.ID, notifyEnq.Sent[1].UserID) - require.Contains(t, notifyEnq.Sent[1].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[1].Labels["activated_account_name"]) - - // "Member" account suspended, "member" notified - require.Equal(t, notifications.TemplateYourAccountActivated, notifyEnq.Sent[2].TemplateID) - require.Equal(t, member.ID, notifyEnq.Sent[2].UserID) - require.Contains(t, notifyEnq.Sent[2].Targets, member.ID) - require.Equal(t, member.Username, notifyEnq.Sent[2].Labels["activated_account_name"]) + // when + _, err = adminClient.UpdateUserStatus(context.Background(), member.Username, codersdk.UserStatusActive) + require.NoError(t, err) + + // then + verifyNotificationDispatched(notifyEnq, []expectedNotification{ + {TemplateID: notifications.TemplateUserAccountActivated, UserID: firstUser.UserID}, + {TemplateID: notifications.TemplateUserAccountActivated, UserID: userAdmin.ID}, + {TemplateID: notifications.TemplateYourAccountActivated, UserID: member.ID}, + }, member, "activated_account_name") + }) } func TestNotifyDeletedUser(t *testing.T) { From b789a5373d0ef6942caee963da906fb3e06ebc6a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 13:22:45 +0200 Subject: [PATCH 17/18] 247 --- ...nded.down.sql => 000247_notifications_user_suspended.down.sql} | 0 ...uspended.up.sql => 000247_notifications_user_suspended.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000246_notifications_user_suspended.down.sql => 000247_notifications_user_suspended.down.sql} (100%) rename coderd/database/migrations/{000246_notifications_user_suspended.up.sql => 000247_notifications_user_suspended.up.sql} (100%) diff --git a/coderd/database/migrations/000246_notifications_user_suspended.down.sql b/coderd/database/migrations/000247_notifications_user_suspended.down.sql similarity index 100% rename from coderd/database/migrations/000246_notifications_user_suspended.down.sql rename to coderd/database/migrations/000247_notifications_user_suspended.down.sql diff --git a/coderd/database/migrations/000246_notifications_user_suspended.up.sql b/coderd/database/migrations/000247_notifications_user_suspended.up.sql similarity index 100% rename from coderd/database/migrations/000246_notifications_user_suspended.up.sql rename to coderd/database/migrations/000247_notifications_user_suspended.up.sql From 9931bbf49b66588a788b1c1957ea09d015c741d6 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Thu, 22 Aug 2024 13:43:05 +0200 Subject: [PATCH 18/18] Danny's review --- coderd/users.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/coderd/users.go b/coderd/users.go index d676ed28ead81..07ec053ca44a7 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -861,11 +861,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW err = api.notifyUserStatusChanged(ctx, user, status) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error notifying about changed user status.", - Detail: err.Error(), - }) - return + api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err)) } organizations, err := userOrganizationIDs(ctx, api, user) @@ -882,16 +878,16 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, status database.UserStatus) error { var key string - var adminTemplateID, ownerTemplateID uuid.UUID + var adminTemplateID, personalTemplateID uuid.UUID switch status { case database.UserStatusSuspended: key = "suspended_account_name" adminTemplateID = notifications.TemplateUserAccountSuspended - ownerTemplateID = notifications.TemplateYourAccountSuspended + personalTemplateID = notifications.TemplateYourAccountSuspended case database.UserStatusActive: key = "activated_account_name" adminTemplateID = notifications.TemplateUserAccountActivated - ownerTemplateID = notifications.TemplateYourAccountActivated + personalTemplateID = notifications.TemplateYourAccountActivated default: api.Logger.Error(ctx, "user status is not supported", slog.F("username", user.Username), slog.F("user_status", string(status))) return xerrors.Errorf("unable to notify admins as the user's status is unsupported") @@ -899,7 +895,7 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, userAdmins, err := findUserAdmins(ctx, api.Database) if err != nil { - return xerrors.Errorf("unable to find user admins: %w", err) + api.Logger.Error(ctx, "unable to find user admins", slog.Error(err)) } // Send notifications to user admins and affected user @@ -913,7 +909,7 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err)) } } - if _, err := api.NotificationsEnqueuer.Enqueue(ctx, user.ID, ownerTemplateID, + if _, err := api.NotificationsEnqueuer.Enqueue(ctx, user.ID, personalTemplateID, map[string]string{ key: user.Username, }, "api-put-user-status",