Skip to content

Commit c7f2a67

Browse files
committed
fix: Add route for user to change own password
1 parent b6d6276 commit c7f2a67

File tree

5 files changed

+112
-3
lines changed

5 files changed

+112
-3
lines changed

coderd/coderd.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,14 @@ func New(options *Options) *API {
233233
})
234234
r.Route("/{user}", func(r chi.Router) {
235235
r.Use(httpmw.ExtractUserParam(options.Database))
236+
<<<<<<< HEAD
236237
r.Get("/", api.userByName)
237238
r.Put("/profile", api.putUserProfile)
239+
=======
240+
r.Get("/", a.userByName)
241+
r.Put("/profile", a.putUserProfile)
242+
r.Put("/security", a.putUserSecurity)
243+
>>>>>>> fix: Add route for user to change own password
238244
r.Route("/status", func(r chi.Router) {
239245
r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended))
240246
r.Put("/active", api.putUserStatus(database.UserStatusActive))

coderd/roles.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) {
4040
func (api *API) checkPermissions(rw http.ResponseWriter, r *http.Request) {
4141
user := httpmw.UserParam(r)
4242

43-
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithOwner(user.ID.String())) {
43+
if !api.Authorize(rw, r, rbac.ActionRead, rbac.ResourceUser.WithID(user.ID.String())) {
4444
return
4545
}
4646

coderd/userpassword/userpassword.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,19 @@ func hashWithSaltAndIter(password string, salt []byte, iter int) string {
121121

122122
return fmt.Sprintf("$%s$%d$%s$%s", hashScheme, iter, encSalt, encHash)
123123
}
124+
125+
// Validate checks that the plain text password meets the minimum password requirements.
126+
// It returns properly formatted errors for detailed form validation on the client.
127+
func Validate(password string) error {
128+
const (
129+
minLength = 8
130+
maxLength = 64
131+
)
132+
if len(password) < minLength {
133+
return xerrors.Errorf("Password must be at least %d characters.", minLength)
134+
}
135+
if len(password) > maxLength {
136+
return xerrors.Errorf("Password must be no more than %d characters.", maxLength)
137+
}
138+
return nil
139+
}

coderd/users.go

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,75 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
311311
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs))
312312
}
313313

314-
func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) {
314+
func (api *API) putUserSecurity(rw http.ResponseWriter, r *http.Request) {
315+
user := httpmw.UserParam(r)
316+
317+
// this route is for the owning user so we need to check the old password
318+
// to protect against a compromised session being able to change the user's password.
319+
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) {
320+
return
321+
}
322+
323+
var params codersdk.UpdateUserSecurityRequest
324+
if !httpapi.Read(rw, r, &params) {
325+
return
326+
}
327+
328+
ok, err := userpassword.Compare(string(user.HashedPassword), params.OldPassword)
329+
if err != nil {
330+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
331+
Message: fmt.Sprintf("compare user password: %s", err.Error()),
332+
})
333+
return
334+
}
335+
if !ok {
336+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
337+
Errors: []httpapi.Error{
338+
{
339+
Field: "old_password",
340+
Detail: "Old password is incorrect.",
341+
},
342+
},
343+
})
344+
return
345+
}
346+
347+
err = userpassword.Validate(params.Password)
348+
if err != nil {
349+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
350+
Errors: []httpapi.Error{
351+
{
352+
Field: "password",
353+
Detail: err.Error(),
354+
},
355+
},
356+
})
357+
return
358+
}
359+
360+
hashedPassword, err := userpassword.Hash(params.Password)
361+
if err != nil {
362+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
363+
Message: fmt.Sprintf("hash password: %s", err.Error()),
364+
})
365+
return
366+
}
367+
368+
err = api.Database.UpdateUserHashedPassword(r.Context(), database.UpdateUserHashedPasswordParams{
369+
ID: user.ID,
370+
HashedPassword: []byte(hashedPassword),
371+
})
372+
if err != nil {
373+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
374+
Message: fmt.Sprintf("put user password: %s", err.Error()),
375+
})
376+
return
377+
}
378+
379+
httpapi.Write(rw, http.StatusNoContent, nil)
380+
}
381+
382+
func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) {
315383
return func(rw http.ResponseWriter, r *http.Request) {
316384
user := httpmw.UserParam(r)
317385
apiKey := httpmw.APIKey(r)
@@ -358,14 +426,28 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
358426
params codersdk.UpdateUserPasswordRequest
359427
)
360428

361-
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) {
429+
// this route is for admins so we don't need to require an old password.
430+
if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUser.WithID(user.ID.String())) {
362431
return
363432
}
364433

365434
if !httpapi.Read(rw, r, &params) {
366435
return
367436
}
368437

438+
err := userpassword.Validate(params.Password)
439+
if err != nil {
440+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
441+
Errors: []httpapi.Error{
442+
{
443+
Field: "password",
444+
Detail: err.Error(),
445+
},
446+
},
447+
})
448+
return
449+
}
450+
369451
hashedPassword, err := userpassword.Hash(params.Password)
370452
if err != nil {
371453
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{

codersdk/users.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ type UpdateUserPasswordRequest struct {
6868
Password string `json:"password" validate:"required"`
6969
}
7070

71+
type UpdateUserSecurityRequest struct {
72+
OldPassword string `json:"old_password" validate:"required"`
73+
Password string `json:"password" validate:"required"`
74+
}
75+
7176
type UpdateRoles struct {
7277
Roles []string `json:"roles" validate:"required"`
7378
}

0 commit comments

Comments
 (0)