Skip to content

feat(coderd/notifications): improve notification format consistency #14967

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

Merged
merged 20 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
70f3486
feat(notifications): Improve notification format consistency
SasSwart Oct 3, 2024
7e31a34
chore(coderd/notifications): regenerate notification testdata from th…
SasSwart Oct 5, 2024
cf3afd4
Merge remote-tracking branch 'origin/main' into jjs/consistent-notifi…
SasSwart Oct 5, 2024
4b85f2b
chore(coderd/database): renumber migration
SasSwart Oct 5, 2024
e8ad3ac
chore(coderd/notifications): regenerate testdata
SasSwart Oct 5, 2024
2a4d740
fix(coderd/notifications): remove duplicate function signature
SasSwart Oct 5, 2024
e741c43
chore: remove redundant escaping in migration
SasSwart Oct 5, 2024
adffe60
chore(coderd/notifications): improve failed test feedback
SasSwart Oct 5, 2024
41ed54a
feat(coderd/database): add new information to the account activated n…
SasSwart Oct 7, 2024
5541331
Merge remote-tracking branch 'origin/main' into jjs/additional-notifi…
SasSwart Oct 8, 2024
fe94f0d
chore(coderd/database): rework migration for legibility
SasSwart Oct 8, 2024
98e7501
feat(coderd): send newly required information to notification templates
SasSwart Oct 8, 2024
d8e00c2
feat(coderd/notifications): provide additional context to workspace n…
SasSwart Oct 8, 2024
d6a339f
fix(coderd/notifications): add a missing call to fmt.Sprintf
SasSwart Oct 8, 2024
920ad31
fix(coderd/notifications): fix oversights in template migration
SasSwart Oct 9, 2024
9e938e5
chore(coderd/provisionerdserver): set the displayname in TestNotifica…
SasSwart Oct 9, 2024
59e57ac
chore(coderd): add more robust testing assertions to TestNotifyDelete…
SasSwart Oct 9, 2024
c907238
chore(coderd/notifications): fix migration indentation
SasSwart Oct 9, 2024
2493556
chore(coderd/notifications): regenerate golden files
SasSwart Oct 9, 2024
19dccc8
Merge remote-tracking branch 'origin/main' into jjs/consistent-notifi…
SasSwart Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(coderd): send newly required information to notification templates
  • Loading branch information
SasSwart committed Oct 8, 2024
commit 98e7501840a51913eb6b56d55329f14eac5865a1
5 changes: 3 additions & 2 deletions coderd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ func (api *API) notifyTemplateDeleted(ctx context.Context, template database.Tem

if _, err := api.NotificationsEnqueuer.Enqueue(ctx, receiverID, notifications.TemplateTemplateDeleted,
map[string]string{
"name": template.Name,
"initiator": initiator.Username,
"name": template.Name,
"display_name": template.DisplayName,
"initiator": initiator.Username,
}, "api-templates-delete",
// Associate this notification with all the related entities.
template.ID, template.OrganizationID,
Expand Down
5 changes: 4 additions & 1 deletion coderd/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1419,7 +1419,9 @@ func TestTemplateNotifications(t *testing.T) {
// Setup template
version = coderdtest.CreateTemplateVersion(t, client, initiator.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template = coderdtest.CreateTemplate(t, client, initiator.OrganizationID, version.ID)
template = coderdtest.CreateTemplate(t, client, initiator.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DisplayName = "Bobby's Template"
})
)

// Setup users with different roles
Expand Down Expand Up @@ -1455,6 +1457,7 @@ func TestTemplateNotifications(t *testing.T) {
require.Contains(t, n.Targets, template.ID)
require.Contains(t, n.Targets, template.OrganizationID)
require.Equal(t, n.Labels["name"], template.Name)
require.Equal(t, n.Labels["display_name"], template.DisplayName)
require.Equal(t, n.Labels["initiator"], coderdtest.FirstUserParams.Username)
}
})
Expand Down
5 changes: 3 additions & 2 deletions coderd/userauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1481,14 +1481,15 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
Username: params.Username,
OrganizationIDs: orgIDs,
},
LoginType: params.LoginType,
LoginType: params.LoginType,
accountCreatorName: "oauth",
})
if err != nil {
return xerrors.Errorf("create user: %w", err)
}
}

// Activate dormant user on sigin
// Activate dormant user on sign-in
if user.Status == database.UserStatusDormant {
//nolint:gocritic // System needs to update status of the user account (dormant -> active).
user, err = tx.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
Expand Down
89 changes: 66 additions & 23 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
Password: createUser.Password,
OrganizationIDs: []uuid.UUID{defaultOrg.ID},
},
LoginType: database.LoginTypePassword,
LoginType: database.LoginTypePassword,
accountCreatorName: "coder",
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Expand Down Expand Up @@ -479,10 +480,22 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
return
}

apiKey := httpmw.APIKey(r)

accountCreator, err := api.Database.GetUserByID(ctx, apiKey.UserID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Unable to determine the details of the actor creating the account.",
})
return
}

user, err := api.CreateUser(ctx, api.Database, CreateUserRequest{
CreateUserRequestWithOrgs: req,
LoginType: loginType,
accountCreatorName: accountCreator.Name,
})

if dbauthz.IsNotAuthorizedError(err) {
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
Message: "You are not authorized to create users.",
Expand Down Expand Up @@ -576,11 +589,24 @@ func (api *API) deleteUser(rw http.ResponseWriter, r *http.Request) {
return
}

apiKey := httpmw.APIKey(r)

accountDeleter, err := api.Database.GetUserByID(ctx, apiKey.UserID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow-up PR, this feels like something that we should inject into the context.

if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Unable to determine the details of the actor deleting the account.",
})
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",
"deleted_account_name": user.Username,
"deleted_account_user_name": user.Name,
"account_deleter_user_name": accountDeleter.Name,
},
"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))
Expand Down Expand Up @@ -844,6 +870,14 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
}
}

actingUser, err := api.Database.GetUserByID(ctx, apiKey.UserID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Unable to determine the details of the actor creating the account.",
})
return
}

targetUser, err := api.Database.UpdateUserStatus(ctx, database.UpdateUserStatusParams{
ID: user.ID,
Status: status,
Expand All @@ -858,7 +892,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
}
aReq.New = targetUser

err = api.notifyUserStatusChanged(ctx, user, status)
err = api.notifyUserStatusChanged(ctx, actingUser.Name, user, status)
if err != nil {
api.Logger.Warn(ctx, "unable to notify about changed user's status", slog.F("affected_user", user.Username), slog.Error(err))
}
Expand All @@ -871,24 +905,33 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
})
return
}

httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(targetUser, organizations))
}
}

func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User, status database.UserStatus) error {
var key string
func (api *API) notifyUserStatusChanged(ctx context.Context, actingUserName string, targetUser database.User, status database.UserStatus) error {
var labels map[string]string
var adminTemplateID, personalTemplateID uuid.UUID
switch status {
case database.UserStatusSuspended:
key = "suspended_account_name"
labels = map[string]string{
"suspended_account_name": targetUser.Username,
"suspended_account_user_name": targetUser.Name,
"account_suspender_user_name": actingUserName,
}
adminTemplateID = notifications.TemplateUserAccountSuspended
personalTemplateID = notifications.TemplateYourAccountSuspended
case database.UserStatusActive:
key = "activated_account_name"
labels = map[string]string{
"activated_account_name": targetUser.Username,
"activated_account_user_name": targetUser.Name,
"account_activator_user_name": actingUserName,
}
adminTemplateID = notifications.TemplateUserAccountActivated
personalTemplateID = notifications.TemplateYourAccountActivated
default:
api.Logger.Error(ctx, "user status is not supported", slog.F("username", user.Username), slog.F("user_status", string(status)))
api.Logger.Error(ctx, "user status is not supported", slog.F("username", targetUser.Username), slog.F("user_status", string(status)))
return xerrors.Errorf("unable to notify admins as the user's status is unsupported")
}

Expand All @@ -900,21 +943,17 @@ func (api *API) notifyUserStatusChanged(ctx context.Context, user database.User,
// Send notifications to user admins and affected user
for _, u := range userAdmins {
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, adminTemplateID,
map[string]string{
key: user.Username,
}, "api-put-user-status",
user.ID,
labels, "api-put-user-status",
targetUser.ID,
); err != nil {
api.Logger.Warn(ctx, "unable to notify about changed user's 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", targetUser.Username), slog.Error(err))
}
}
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, user.ID, personalTemplateID,
map[string]string{
key: user.Username,
}, "api-put-user-status",
user.ID,
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, targetUser.ID, personalTemplateID,
labels, "api-put-user-status",
targetUser.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))
api.Logger.Warn(ctx, "unable to notify user about status change of their account", slog.F("affected_user", targetUser.Username), slog.Error(err))
}
return nil
}
Expand Down Expand Up @@ -1280,8 +1319,9 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques

type CreateUserRequest struct {
codersdk.CreateUserRequestWithOrgs
LoginType database.LoginType
SkipNotifications bool
LoginType database.LoginType
SkipNotifications bool
accountCreatorName string
}

func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) {
Expand Down Expand Up @@ -1365,13 +1405,16 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
for _, u := range userAdmins {
if _, err := api.NotificationsEnqueuer.Enqueue(ctx, u.ID, notifications.TemplateUserAccountCreated,
map[string]string{
"created_account_name": user.Username,
"created_account_name": user.Username,
"created_account_user_name": user.Name,
"account_creator": req.accountCreatorName,
}, "api-users-create",
user.ID,
); err != nil {
api.Logger.Warn(ctx, "unable to notify about created user", slog.F("created_user", user.Username), slog.Error(err))
}
}

return user, err
}

Expand Down
3 changes: 3 additions & 0 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,9 @@ func TestNotifyDeletedUser(t *testing.T) {
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"])
require.Equal(t, user.Name, notifyEnq.Sent[1].Labels["deleted_account_user_name"])
// Not sure where to get the following just yet
// require.Equal(t, , notifyEnq.Sent[1].Labels["account_deleter_user_name"])
})

t.Run("UserAdminNotified", func(t *testing.T) {
Expand Down
Loading