Skip to content

fix: Add route for user to change own password #1812

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 10 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
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
2 changes: 1 addition & 1 deletion coderd/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) {
func (api *API) checkPermissions(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)

if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithOwner(user.ID.String())) {
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithID(user.ID.String())) {
return
}

Expand Down
16 changes: 16 additions & 0 deletions coderd/userpassword/userpassword.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,19 @@ func hashWithSaltAndIter(password string, salt []byte, iter int) string {

return fmt.Sprintf("$%s$%d$%s$%s", hashScheme, iter, encSalt, encHash)
}

// Validate checks that the plain text password meets the minimum password requirements.
// It returns properly formatted errors for detailed form validation on the client.
func Validate(password string) error {
const (
minLength = 8
maxLength = 64
)
if len(password) < minLength {
return xerrors.Errorf("Password must be at least %d characters.", minLength)
}
if len(password) > maxLength {
return xerrors.Errorf("Password must be no more than %d characters.", maxLength)
}
return nil
}
38 changes: 38 additions & 0 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
var (
user = httpmw.UserParam(r)
apiKey = httpmw.APIKey(r)
params codersdk.UpdateUserPasswordRequest
)

Expand All @@ -366,6 +367,43 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
return
}

// we want to require old_password field if the user is changing their
// own password. This is to prevent a compromised session from being able
// to change password and lock out the user.
Comment on lines +383 to +385
Copy link
Member

Choose a reason for hiding this comment

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

Is this security through obscurity? A highjacked admin session could still reset everyone's passwords.

Copy link
Contributor Author

@f0ssel f0ssel May 27, 2022

Choose a reason for hiding this comment

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

Not exactly, it's to prevent a leaked admin token from being able to lock the admin out. They could reset other people's passwords, but not the admin themselves. It's a small scope but makes sense to do I think.

Copy link
Member

Choose a reason for hiding this comment

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

It's mainly reusing the same endpoint for reset & change. I think the security implications are not even a consideration at this stage. Admins have some super powers that we don't need to tighten for MVP

if user.ID == apiKey.UserID {
ok, err := userpassword.Compare(string(user.HashedPassword), params.OldPassword)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("compare user password: %s", err.Error()),
})
return
}
if !ok {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Errors: []httpapi.Error{
{
Field: "old_password",
Detail: "Old password is incorrect.",
},
},
})
return
}
}

err := userpassword.Validate(params.Password)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Errors: []httpapi.Error{
{
Field: "password",
Detail: err.Error(),
},
},
})
return
}

hashedPassword, err := userpassword.Hash(params.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand Down
30 changes: 30 additions & 0 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,36 @@ func TestUpdateUserPassword(t *testing.T) {
})
require.NoError(t, err, "member should login successfully with the new password")
})
t.Run("MemberCanUpdateOwnPassword", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
admin := coderdtest.CreateFirstUser(t, client)
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
err := member.UpdateUserPassword(context.Background(), "me", codersdk.UpdateUserPasswordRequest{
OldPassword: "testpass",
Password: "newpassword",
})
require.NoError(t, err, "member should be able to update own password")
})
t.Run("MemberCantUpdateOwnPasswordWithoutOldPassword", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
admin := coderdtest.CreateFirstUser(t, client)
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
err := member.UpdateUserPassword(context.Background(), "me", codersdk.UpdateUserPasswordRequest{
Password: "newpassword",
})
require.Error(t, err, "member should not be able to update own password without providing old password")
})
t.Run("AdminCantUpdateOwnPasswordWithoutOldPassword", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
err := client.UpdateUserPassword(context.Background(), "me", codersdk.UpdateUserPasswordRequest{
Password: "newpassword",
})
require.Error(t, err, "admin should not be able to update own password without providing old password")
})
}

func TestGrantRoles(t *testing.T) {
Expand Down
3 changes: 2 additions & 1 deletion codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ type UpdateUserProfileRequest struct {
}

type UpdateUserPasswordRequest struct {
Password string `json:"password" validate:"required"`
OldPassword string `json:"old_password" validate:""`
Password string `json:"password" validate:"required"`
}

type UpdateRoles struct {
Expand Down
23 changes: 12 additions & 11 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface AgentGitSSHKey {
readonly private_key: string
}

// From codersdk/users.go:151:6
// From codersdk/users.go:152:6
export interface AuthMethods {
readonly password: boolean
readonly github: boolean
Expand Down Expand Up @@ -44,7 +44,7 @@ export interface CreateFirstUserResponse {
readonly organization_id: string
}

// From codersdk/users.go:146:6
// From codersdk/users.go:147:6
export interface CreateOrganizationRequest {
readonly name: string
}
Expand Down Expand Up @@ -100,7 +100,7 @@ export interface CreateWorkspaceRequest {
readonly parameter_values?: CreateParameterRequest[]
}

// From codersdk/users.go:142:6
// From codersdk/users.go:143:6
export interface GenerateAPIKeyResponse {
readonly key: string
}
Expand All @@ -118,13 +118,13 @@ export interface GoogleInstanceIdentityToken {
readonly json_web_token: string
}

// From codersdk/users.go:131:6
// From codersdk/users.go:132:6
export interface LoginWithPasswordRequest {
readonly email: string
readonly password: string
}

// From codersdk/users.go:137:6
// From codersdk/users.go:138:6
export interface LoginWithPasswordResponse {
readonly session_token: string
}
Expand Down Expand Up @@ -276,13 +276,14 @@ export interface UpdateActiveTemplateVersion {
readonly id: string
}

// From codersdk/users.go:71:6
// From codersdk/users.go:72:6
export interface UpdateRoles {
readonly roles: string[]
}

// From codersdk/users.go:67:6
export interface UpdateUserPasswordRequest {
readonly old_password: string
readonly password: string
}

Expand Down Expand Up @@ -319,29 +320,29 @@ export interface User {
readonly roles: Role[]
}

// From codersdk/users.go:96:6
// From codersdk/users.go:97:6
export interface UserAuthorization {
readonly object: UserAuthorizationObject
readonly action: string
}

// From codersdk/users.go:112:6
// From codersdk/users.go:113:6
export interface UserAuthorizationObject {
readonly resource_type: string
readonly owner_id?: string
readonly organization_id?: string
readonly resource_id?: string
}

// From codersdk/users.go:85:6
// From codersdk/users.go:86:6
export interface UserAuthorizationRequest {
readonly checks: Record<string, UserAuthorization>
}

// From codersdk/users.go:80:6
// From codersdk/users.go:81:6
export type UserAuthorizationResponse = Record<string, boolean>

// From codersdk/users.go:75:6
// From codersdk/users.go:76:6
export interface UserRoles {
readonly roles: string[]
readonly organization_roles: Record<string, string[]>
Expand Down