From 3b7584660ccd1bd7b1935b7a024254c2375f62f7 Mon Sep 17 00:00:00 2001 From: Garrett Date: Mon, 23 May 2022 21:16:10 +0000 Subject: [PATCH 01/10] fix: Add route for user to change own password --- coderd/coderd.go | 6 ++ coderd/roles.go | 2 +- coderd/userpassword/userpassword.go | 16 ++++++ coderd/users.go | 86 ++++++++++++++++++++++++++++- codersdk/users.go | 5 ++ 5 files changed, 112 insertions(+), 3 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index c8c292a8a35b1..0adf9fff94432 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -240,8 +240,14 @@ func New(options *Options) *API { }) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) +<<<<<<< HEAD r.Get("/", api.userByName) r.Put("/profile", api.putUserProfile) +======= + r.Get("/", a.userByName) + r.Put("/profile", a.putUserProfile) + r.Put("/security", a.putUserSecurity) +>>>>>>> fix: Add route for user to change own password r.Route("/status", func(r chi.Router) { r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended)) r.Put("/active", api.putUserStatus(database.UserStatusActive)) 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..29eda199c77bf 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -311,7 +311,75 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs)) } -func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) { +func (api *API) putUserSecurity(rw http.ResponseWriter, r *http.Request) { + user := httpmw.UserParam(r) + + // this route is for the owning user so we need to check the old password + // to protect against a compromised session being able to change the user's password. + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) { + return + } + + var params codersdk.UpdateUserSecurityRequest + if !httpapi.Read(rw, r, ¶ms) { + return + } + + 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{ + Message: fmt.Sprintf("hash password: %s", err.Error()), + }) + return + } + + err = api.Database.UpdateUserHashedPassword(r.Context(), database.UpdateUserHashedPasswordParams{ + ID: user.ID, + HashedPassword: []byte(hashedPassword), + }) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("put user password: %s", err.Error()), + }) + return + } + + httpapi.Write(rw, http.StatusNoContent, nil) +} + +func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) { return func(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) apiKey := httpmw.APIKey(r) @@ -358,7 +426,8 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) { params codersdk.UpdateUserPasswordRequest ) - if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) { + // this route is for admins so we don't need to require an old password. + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUser.WithID(user.ID.String())) { return } @@ -366,6 +435,19 @@ 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 + } + hashedPassword, err := userpassword.Hash(params.Password) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ diff --git a/codersdk/users.go b/codersdk/users.go index f594bf2b375b7..ab977fdd9e70e 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -68,6 +68,11 @@ type UpdateUserPasswordRequest struct { Password string `json:"password" validate:"required"` } +type UpdateUserSecurityRequest struct { + OldPassword string `json:"old_password" validate:"required"` + Password string `json:"password" validate:"required"` +} + type UpdateRoles struct { Roles []string `json:"roles" validate:"required"` } From c0ac5de76e4fd198c95233052a8425f4b9f8c2f2 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 26 May 2022 21:06:38 +0000 Subject: [PATCH 02/10] change to ownpassword --- coderd/coderd.go | 4 ++++ coderd/users.go | 6 +++++- codersdk/users.go | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 0adf9fff94432..85475f2c1df9f 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -246,8 +246,11 @@ func New(options *Options) *API { ======= r.Get("/", a.userByName) r.Put("/profile", a.putUserProfile) +<<<<<<< HEAD r.Put("/security", a.putUserSecurity) >>>>>>> fix: Add route for user to change own password +======= +>>>>>>> change to ownpassword r.Route("/status", func(r chi.Router) { r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended)) r.Put("/active", api.putUserStatus(database.UserStatusActive)) @@ -255,6 +258,7 @@ func New(options *Options) *API { r.Route("/password", func(r chi.Router) { r.Put("/", api.putUserPassword) }) + r.Put("/ownpassword", a.putUserOwnPassword) // These roles apply to the site wide permissions. r.Put("/roles", api.putUserRoles) r.Get("/roles", api.userRoles) diff --git a/coderd/users.go b/coderd/users.go index 29eda199c77bf..9e477b9a221f6 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -311,7 +311,11 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs)) } +<<<<<<< HEAD func (api *API) putUserSecurity(rw http.ResponseWriter, r *http.Request) { +======= +func (api *api) putUserOwnPassword(rw http.ResponseWriter, r *http.Request) { +>>>>>>> change to ownpassword user := httpmw.UserParam(r) // this route is for the owning user so we need to check the old password @@ -320,7 +324,7 @@ func (api *API) putUserSecurity(rw http.ResponseWriter, r *http.Request) { return } - var params codersdk.UpdateUserSecurityRequest + var params codersdk.UpdateUserOwnPasswordRequest if !httpapi.Read(rw, r, ¶ms) { return } diff --git a/codersdk/users.go b/codersdk/users.go index ab977fdd9e70e..83047bd8baff2 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -68,7 +68,7 @@ type UpdateUserPasswordRequest struct { Password string `json:"password" validate:"required"` } -type UpdateUserSecurityRequest struct { +type UpdateUserOwnPasswordRequest struct { OldPassword string `json:"old_password" validate:"required"` Password string `json:"password" validate:"required"` } From ff2b801f5cc12cd99b5e9de6400839abc1bef113 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 26 May 2022 21:13:18 +0000 Subject: [PATCH 03/10] fix --- coderd/coderd.go | 11 +---------- coderd/users.go | 8 ++------ 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 85475f2c1df9f..09c5d51b6569d 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -240,17 +240,8 @@ func New(options *Options) *API { }) r.Route("/{user}", func(r chi.Router) { r.Use(httpmw.ExtractUserParam(options.Database)) -<<<<<<< HEAD r.Get("/", api.userByName) r.Put("/profile", api.putUserProfile) -======= - r.Get("/", a.userByName) - r.Put("/profile", a.putUserProfile) -<<<<<<< HEAD - r.Put("/security", a.putUserSecurity) ->>>>>>> fix: Add route for user to change own password -======= ->>>>>>> change to ownpassword r.Route("/status", func(r chi.Router) { r.Put("/suspend", api.putUserStatus(database.UserStatusSuspended)) r.Put("/active", api.putUserStatus(database.UserStatusActive)) @@ -258,7 +249,7 @@ func New(options *Options) *API { r.Route("/password", func(r chi.Router) { r.Put("/", api.putUserPassword) }) - r.Put("/ownpassword", a.putUserOwnPassword) + r.Put("/ownpassword", api.putUserOwnPassword) // These roles apply to the site wide permissions. r.Put("/roles", api.putUserRoles) r.Get("/roles", api.userRoles) diff --git a/coderd/users.go b/coderd/users.go index 9e477b9a221f6..57583f341302e 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -311,11 +311,7 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs)) } -<<<<<<< HEAD -func (api *API) putUserSecurity(rw http.ResponseWriter, r *http.Request) { -======= -func (api *api) putUserOwnPassword(rw http.ResponseWriter, r *http.Request) { ->>>>>>> change to ownpassword +func (api *API) putUserOwnPassword(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) // this route is for the owning user so we need to check the old password @@ -383,7 +379,7 @@ func (api *api) putUserOwnPassword(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusNoContent, nil) } -func (api *api) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) { +func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) { return func(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) apiKey := httpmw.APIKey(r) From a4902925c3cbef6b1f4d6cb2298f3e97ffdac417 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 26 May 2022 21:35:34 +0000 Subject: [PATCH 04/10] Add basic test --- coderd/users_test.go | 26 ++++++++++++++++++++++++++ codersdk/users.go | 13 +++++++++++++ 2 files changed, 39 insertions(+) diff --git a/coderd/users_test.go b/coderd/users_test.go index d8d05542df092..5163bd4307fe5 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -324,6 +324,32 @@ func TestUpdateUserPassword(t *testing.T) { }) require.NoError(t, err, "member should login successfully with the new password") }) + + t.Run("MemberCantUpdateOwnPasswordViaAdminRoute", 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 via admin route") + }) +} + +func TestUpdateUserOwnPassword(t *testing.T) { + t.Parallel() + 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.UpdateUserOwnPassword(context.Background(), codersdk.UpdateUserOwnPasswordRequest{ + OldPassword: "testpass", + Password: "newpassword", + }) + require.NoError(t, err, "member should be able to update own password") + }) } func TestGrantRoles(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index 83047bd8baff2..991ee9ec427d3 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -443,3 +443,16 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { var userAuth AuthMethods return userAuth, json.NewDecoder(res.Body).Decode(&userAuth) } + +// UpdateUserOwnPassword updates a user's own password. +func (c *Client) UpdateUserOwnPassword(ctx context.Context, req UpdateUserOwnPasswordRequest) error { + res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/me/ownpassword"), req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusNoContent { + return readBodyAsError(res) + } + return nil +} From 5a224eb4b2f3f4f849c90029ddf610862d3f6527 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 26 May 2022 21:39:02 +0000 Subject: [PATCH 05/10] lint --- codersdk/users.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codersdk/users.go b/codersdk/users.go index 991ee9ec427d3..cfc978202743b 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -446,7 +446,7 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { // UpdateUserOwnPassword updates a user's own password. func (c *Client) UpdateUserOwnPassword(ctx context.Context, req UpdateUserOwnPasswordRequest) error { - res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/me/ownpassword"), req) + res, err := c.Request(ctx, http.MethodPut, "/api/v2/users/me/ownpassword", req) if err != nil { return err } From 9ea0e20bfb82f16a80e97008004286a8ca044f5f Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 26 May 2022 21:44:27 +0000 Subject: [PATCH 06/10] make gen --- site/src/api/typesGenerated.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 57bc83ebc6dc3..ad107bfffc264 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:156: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:151: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:147: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:136:6 export interface LoginWithPasswordRequest { readonly email: string readonly password: string } -// From codersdk/users.go:137:6 +// From codersdk/users.go:142:6 export interface LoginWithPasswordResponse { readonly session_token: string } @@ -276,11 +276,17 @@ export interface UpdateActiveTemplateVersion { readonly id: string } -// From codersdk/users.go:71:6 +// From codersdk/users.go:76:6 export interface UpdateRoles { readonly roles: string[] } +// From codersdk/users.go:71:6 +export interface UpdateUserOwnPasswordRequest { + readonly old_password: string + readonly password: string +} + // From codersdk/users.go:67:6 export interface UpdateUserPasswordRequest { readonly password: string @@ -319,13 +325,13 @@ export interface User { readonly roles: Role[] } -// From codersdk/users.go:96:6 +// From codersdk/users.go:101:6 export interface UserAuthorization { readonly object: UserAuthorizationObject readonly action: string } -// From codersdk/users.go:112:6 +// From codersdk/users.go:117:6 export interface UserAuthorizationObject { readonly resource_type: string readonly owner_id?: string @@ -333,15 +339,15 @@ export interface UserAuthorizationObject { readonly resource_id?: string } -// From codersdk/users.go:85:6 +// From codersdk/users.go:90:6 export interface UserAuthorizationRequest { readonly checks: Record } -// From codersdk/users.go:80:6 +// From codersdk/users.go:85:6 export type UserAuthorizationResponse = Record -// From codersdk/users.go:75:6 +// From codersdk/users.go:80:6 export interface UserRoles { readonly roles: string[] readonly organization_roles: Record From 6454015394ab198942fc13206025997d7394384d Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 27 May 2022 16:32:51 +0000 Subject: [PATCH 07/10] merge into single route --- coderd/coderd.go | 1 - coderd/users.go | 96 ++++++++++++-------------------------------- coderd/users_test.go | 24 +++++------ codersdk/users.go | 19 +-------- 4 files changed, 36 insertions(+), 104 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 09c5d51b6569d..c8c292a8a35b1 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -249,7 +249,6 @@ func New(options *Options) *API { r.Route("/password", func(r chi.Router) { r.Put("/", api.putUserPassword) }) - r.Put("/ownpassword", api.putUserOwnPassword) // These roles apply to the site wide permissions. r.Put("/roles", api.putUserRoles) r.Get("/roles", api.userRoles) diff --git a/coderd/users.go b/coderd/users.go index 57583f341302e..9f772538e5fe1 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -311,74 +311,6 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) { httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs)) } -func (api *API) putUserOwnPassword(rw http.ResponseWriter, r *http.Request) { - user := httpmw.UserParam(r) - - // this route is for the owning user so we need to check the old password - // to protect against a compromised session being able to change the user's password. - if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) { - return - } - - var params codersdk.UpdateUserOwnPasswordRequest - if !httpapi.Read(rw, r, ¶ms) { - return - } - - 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{ - Message: fmt.Sprintf("hash password: %s", err.Error()), - }) - return - } - - err = api.Database.UpdateUserHashedPassword(r.Context(), database.UpdateUserHashedPasswordParams{ - ID: user.ID, - HashedPassword: []byte(hashedPassword), - }) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("put user password: %s", err.Error()), - }) - return - } - - httpapi.Write(rw, http.StatusNoContent, nil) -} - func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) { return func(rw http.ResponseWriter, r *http.Request) { user := httpmw.UserParam(r) @@ -423,11 +355,11 @@ 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 ) - // this route is for admins so we don't need to require an old password. - if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUser.WithID(user.ID.String())) { + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithID(user.ID.String())) { return } @@ -435,6 +367,30 @@ 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. + 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{ diff --git a/coderd/users_test.go b/coderd/users_test.go index 5163bd4307fe5..c1e6071f272b1 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -324,31 +324,25 @@ func TestUpdateUserPassword(t *testing.T) { }) require.NoError(t, err, "member should login successfully with the new password") }) - - t.Run("MemberCantUpdateOwnPasswordViaAdminRoute", func(t *testing.T) { + 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{ - Password: "newpassword", + OldPassword: "testpass", + Password: "newpassword", }) - require.Error(t, err, "member should not be able to update own password via admin route") + require.NoError(t, err, "member should be able to update own password") }) -} - -func TestUpdateUserOwnPassword(t *testing.T) { - t.Parallel() - t.Run("MemberCanUpdateOwnPassword", func(t *testing.T) { + t.Run("AdminCantUpdateOwnPasswordWithoutOldPassword", 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.UpdateUserOwnPassword(context.Background(), codersdk.UpdateUserOwnPasswordRequest{ - OldPassword: "testpass", - Password: "newpassword", + _ = coderdtest.CreateFirstUser(t, client) + err := client.UpdateUserPassword(context.Background(), "me", codersdk.UpdateUserPasswordRequest{ + Password: "newpassword", }) - require.NoError(t, err, "member should be able to update own password") + require.Error(t, err, "admin should not be able to update own password without providing old password") }) } diff --git a/codersdk/users.go b/codersdk/users.go index cfc978202743b..c41b660b4b3ec 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -65,11 +65,7 @@ type UpdateUserProfileRequest struct { } type UpdateUserPasswordRequest struct { - Password string `json:"password" validate:"required"` -} - -type UpdateUserOwnPasswordRequest struct { - OldPassword string `json:"old_password" validate:"required"` + OldPassword string `json:"old_password" validate:""` Password string `json:"password" validate:"required"` } @@ -443,16 +439,3 @@ func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { var userAuth AuthMethods return userAuth, json.NewDecoder(res.Body).Decode(&userAuth) } - -// UpdateUserOwnPassword updates a user's own password. -func (c *Client) UpdateUserOwnPassword(ctx context.Context, req UpdateUserOwnPasswordRequest) error { - res, err := c.Request(ctx, http.MethodPut, "/api/v2/users/me/ownpassword", req) - if err != nil { - return err - } - defer res.Body.Close() - if res.StatusCode != http.StatusNoContent { - return readBodyAsError(res) - } - return nil -} From d86b9b27a6537333fcb1c4cf3dc7ff58ce4cbe52 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 27 May 2022 16:42:42 +0000 Subject: [PATCH 08/10] fix rbac --- coderd/users.go | 2 +- coderd/users_test.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/coderd/users.go b/coderd/users.go index 9f772538e5fe1..c2c888fdf4673 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -359,7 +359,7 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) { params codersdk.UpdateUserPasswordRequest ) - if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithID(user.ID.String())) { + if !api.Authorize(rw, r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) { return } diff --git a/coderd/users_test.go b/coderd/users_test.go index c1e6071f272b1..8e7ea9e827850 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -335,6 +335,16 @@ func TestUpdateUserPassword(t *testing.T) { }) 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) From f1e430aeea45a9964fd08ee02a4d13e240756704 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 27 May 2022 16:46:54 +0000 Subject: [PATCH 09/10] make gen --- site/src/api/typesGenerated.ts | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ad107bfffc264..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:156: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:151: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:147: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:136:6 +// From codersdk/users.go:132:6 export interface LoginWithPasswordRequest { readonly email: string readonly password: string } -// From codersdk/users.go:142:6 +// From codersdk/users.go:138:6 export interface LoginWithPasswordResponse { readonly session_token: string } @@ -276,19 +276,14 @@ export interface UpdateActiveTemplateVersion { readonly id: string } -// From codersdk/users.go:76:6 +// From codersdk/users.go:72:6 export interface UpdateRoles { readonly roles: string[] } -// From codersdk/users.go:71:6 -export interface UpdateUserOwnPasswordRequest { - readonly old_password: string - readonly password: string -} - // From codersdk/users.go:67:6 export interface UpdateUserPasswordRequest { + readonly old_password: string readonly password: string } @@ -325,13 +320,13 @@ export interface User { readonly roles: Role[] } -// From codersdk/users.go:101:6 +// From codersdk/users.go:97:6 export interface UserAuthorization { readonly object: UserAuthorizationObject readonly action: string } -// From codersdk/users.go:117:6 +// From codersdk/users.go:113:6 export interface UserAuthorizationObject { readonly resource_type: string readonly owner_id?: string @@ -339,15 +334,15 @@ export interface UserAuthorizationObject { readonly resource_id?: string } -// From codersdk/users.go:90:6 +// From codersdk/users.go:86:6 export interface UserAuthorizationRequest { readonly checks: Record } -// From codersdk/users.go:85:6 +// From codersdk/users.go:81:6 export type UserAuthorizationResponse = Record -// From codersdk/users.go:80:6 +// From codersdk/users.go:76:6 export interface UserRoles { readonly roles: string[] readonly organization_roles: Record From 1b7ea13671c301656309f4b0dcd01539fbcf85fa Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 27 May 2022 17:17:32 +0000 Subject: [PATCH 10/10] move validate forward --- coderd/users.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/coderd/users.go b/coderd/users.go index c2c888fdf4673..21745c5d786da 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -367,6 +367,19 @@ 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. @@ -391,19 +404,6 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) { } } - 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{