diff --git a/coderd/roles.go b/coderd/roles.go index 3843124fbc45d..eff35eb016516 100644 --- a/coderd/roles.go +++ b/coderd/roles.go @@ -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 } diff --git a/coderd/userpassword/userpassword.go b/coderd/userpassword/userpassword.go index e2c9d41a7763d..ac4d9f6735eaf 100644 --- a/coderd/userpassword/userpassword.go +++ b/coderd/userpassword/userpassword.go @@ -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 +} diff --git a/coderd/users.go b/coderd/users.go index 02d46655d4f54..21745c5d786da 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -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 ) @@ -366,6 +367,43 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) { 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 + } + + // 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. + 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 + } + } + hashedPassword, err := userpassword.Hash(params.Password) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ diff --git a/coderd/users_test.go b/coderd/users_test.go index d8d05542df092..8e7ea9e827850 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -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) { diff --git a/codersdk/users.go b/codersdk/users.go index f594bf2b375b7..c41b660b4b3ec 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -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 { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 57bc83ebc6dc3..74233aa7d9775 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -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 @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -319,13 +320,13 @@ 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 @@ -333,15 +334,15 @@ export interface UserAuthorizationObject { readonly resource_id?: string } -// From codersdk/users.go:85:6 +// From codersdk/users.go:86:6 export interface UserAuthorizationRequest { readonly checks: Record } -// From codersdk/users.go:80:6 +// From codersdk/users.go:81:6 export type UserAuthorizationResponse = Record -// From codersdk/users.go:75:6 +// From codersdk/users.go:76:6 export interface UserRoles { readonly roles: string[] readonly organization_roles: Record