Skip to content
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