Skip to content

fix(coderd): improve password update logic #15210

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 5 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Next Next commit
fix(password): improve logic
  • Loading branch information
defelmnq committed Oct 24, 2024
commit 22ed1292e838ce196c3b101cf226a483a496cf0a
22 changes: 20 additions & 2 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,7 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
ctx = r.Context()
user = httpmw.UserParam(r)
params codersdk.UpdateUserPasswordRequest
apiKey = httpmw.APIKey(r)
auditor = *api.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
Audit: auditor,
Expand Down Expand Up @@ -1045,7 +1046,25 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
return
}

err := userpassword.Validate(params.Password)
admin, err := api.Database.GetUserByID(ctx, apiKey.UserID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching user.",
Detail: err.Error(),
})
return
}
Copy link
Member

Choose a reason for hiding this comment

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

This isn't necessary, you can check the RBAC roles of the user from httpmw.UserParam directly.

Copy link
Contributor Author

@defelmnq defelmnq Oct 24, 2024

Choose a reason for hiding this comment

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

From what I tested the UserParam returns the user from the URL and not the user authenticated - did I missed something ?

What I try to achieve here is to know if the user doing the request is an admin or not.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, good point. I think there are still some subtleties here:

  • httpmw.UserParam will still use the authenticated request user (requestor) from the request context to fetch the required user. If the requestor does not have the permission to read the user, this will fail.
  • This may not take into account custom RBAC roles or an organization admin role

I think the reason for this check is to allow user administrators to reset a user's password without having to specify their existing password.

A better check might be something along the lines of apiKey.UserID != user.ID && api.Authorize(r, policy.ActionUpdate, user.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey @johnstcn as we discussed I changed a bit the logic to verify the user associated to the APIKey to the user we try to update - also validated the behavior for Auditor (it works as expected.)


// only admins or owners can change passwords without sending old_password
if params.OldPassword == "" && (!slice.Contains(admin.RBACRoles, codersdk.RoleUserAdmin) &&
!slice.Contains(admin.RBACRoles, codersdk.RoleOwner)) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Old password is required for non-admin users.",
})
return
}

err = userpassword.Validate(params.Password)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid password.",
Expand All @@ -1059,7 +1078,6 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
return
}

// admins can change passwords without sending old_password
if params.OldPassword != "" {
// if they send something let's validate it
ok, err := userpassword.Compare(string(user.HashedPassword), params.OldPassword)
Expand Down
2 changes: 2 additions & 0 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ func TestUpdateUserPassword(t *testing.T) {
OrganizationIDs: []uuid.UUID{owner.OrganizationID},
})
require.NoError(t, err, "create member")

err = client.UpdateUserPassword(ctx, member.ID.String(), codersdk.UpdateUserPasswordRequest{
Password: "SomeNewStrongPassword!",
})
Expand Down Expand Up @@ -1097,6 +1098,7 @@ func TestUpdateUserPassword(t *testing.T) {
Password: "newpassword",
})
require.Error(t, err, "member should not be able to update own password without providing old password")
require.ErrorContains(t, err, "Old password is required for non-admin users.")
})

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