Skip to content

Commit

Permalink
feat: Add update user password endpoint (#1310)
Browse files Browse the repository at this point in the history
  • Loading branch information
BrunoQuaresma committed May 6, 2022
1 parent a2be7c0 commit 57bb108
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 26 deletions.
4 changes: 4 additions & 0 deletions coderd/coderd.go
Expand Up @@ -240,6 +240,10 @@ func New(options *Options) (http.Handler, func()) {
r.Get("/", api.userByName)
r.Put("/profile", api.putUserProfile)
r.Put("/suspend", api.putUserSuspend)
r.Route("/password", func(r chi.Router) {
r.Use(httpmw.WithRBACObject(rbac.ResourceUserPasswordRole))
r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate))
})
r.Get("/organizations", api.organizationsByUser)
r.Post("/organizations", api.postOrganizationsByUser)
// These roles apply to the site wide permissions.
Expand Down
19 changes: 10 additions & 9 deletions coderd/coderdtest/coderdtest.go
Expand Up @@ -174,21 +174,22 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
return closer
}

var FirstUserParams = codersdk.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
OrganizationName: "testorg",
}

// CreateFirstUser creates a user with preset credentials and authenticates
// with the passed in codersdk client.
func CreateFirstUser(t *testing.T, client *codersdk.Client) codersdk.CreateFirstUserResponse {
req := codersdk.CreateFirstUserRequest{
Email: "testuser@coder.com",
Username: "testuser",
Password: "testpass",
OrganizationName: "testorg",
}
resp, err := client.CreateFirstUser(context.Background(), req)
resp, err := client.CreateFirstUser(context.Background(), FirstUserParams)
require.NoError(t, err)

login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: req.Email,
Password: req.Password,
Email: FirstUserParams.Email,
Password: FirstUserParams.Password,
})
require.NoError(t, err)
client.SessionToken = login.SessionToken
Expand Down
15 changes: 15 additions & 0 deletions coderd/database/databasefake/databasefake.go
Expand Up @@ -1314,6 +1314,21 @@ func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse
return database.User{}, sql.ErrNoRows
}

func (q *fakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()

for i, user := range q.users {
if user.ID != arg.ID {
continue
}
user.HashedPassword = arg.HashedPassword
q.users[i] = user
return nil
}
return sql.ErrNoRows
}

func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
Expand Down
1 change: 1 addition & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion coderd/database/queries/users.sql
Expand Up @@ -59,6 +59,14 @@ WHERE
id = @id
RETURNING *;

-- name: UpdateUserHashedPassword :exec
UPDATE
users
SET
hashed_password = $2
WHERE
id = $1;

-- name: GetUsers :many
SELECT
*
Expand Down Expand Up @@ -133,4 +141,4 @@ FROM
LEFT JOIN organization_members
ON id = user_id
WHERE
id = @user_id;
id = @user_id;
8 changes: 0 additions & 8 deletions coderd/httpmw/userparam.go
Expand Up @@ -76,14 +76,6 @@ func ExtractUserParam(db database.Store) func(http.Handler) http.Handler {
}
}

apiKey := APIKey(r)
if apiKey.UserID != user.ID {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "getting non-personal users isn't supported yet",
})
return
}

ctx := context.WithValue(r.Context(), userParamContextKey{}, user)
next.ServeHTTP(rw, r.WithContext(ctx))
})
Expand Down
4 changes: 4 additions & 0 deletions coderd/rbac/object.go
Expand Up @@ -24,6 +24,10 @@ var (
Type: "user_role",
}

ResourceUserPasswordRole = Object{
Type: "user_password",
}

// ResourceWildcard represents all resource types
ResourceWildcard = Object{
Type: WildcardSymbol,
Expand Down
31 changes: 30 additions & 1 deletion coderd/users.go
Expand Up @@ -360,6 +360,36 @@ func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
}

func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {
var (
user = httpmw.UserParam(r)
params codersdk.UpdateUserPasswordRequest
)
if !httpapi.Read(rw, r, &params) {
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) userRoles(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)

Expand Down Expand Up @@ -577,7 +607,6 @@ func (api *api) postLogin(rw http.ResponseWriter, r *http.Request) {
}

// If the user doesn't exist, it will be a default struct.

equal, err := userpassword.Compare(string(user.HashedPassword), loginWithPassword.Password)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Expand Down
38 changes: 38 additions & 0 deletions coderd/users_test.go
Expand Up @@ -287,6 +287,44 @@ func TestUpdateUserProfile(t *testing.T) {
})
}

func TestUpdateUserPassword(t *testing.T) {
t.Parallel()

t.Run("MemberCantUpdateAdminPassword", 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(), admin.UserID, codersdk.UpdateUserPasswordRequest{
Password: "newpassword",
})
require.Error(t, err, "member should not be able to update admin password")
})

t.Run("AdminCanUpdateMemberPassword", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
admin := coderdtest.CreateFirstUser(t, client)
member, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "coder@coder.com",
Username: "coder",
Password: "password",
OrganizationID: admin.OrganizationID,
})
require.NoError(t, err, "create member")
err = client.UpdateUserPassword(context.Background(), member.ID, codersdk.UpdateUserPasswordRequest{
Password: "newpassword",
})
require.NoError(t, err, "admin should be able to update member password")
// Check if the member can login using the new password
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
Email: "coder@coder.com",
Password: "newpassword",
})
require.NoError(t, err, "member should login successfully with the new password")
})
}

func TestGrantRoles(t *testing.T) {
t.Parallel()
t.Run("UpdateIncorrectRoles", func(t *testing.T) {
Expand Down
18 changes: 18 additions & 0 deletions codersdk/users.go
Expand Up @@ -72,6 +72,10 @@ type UpdateUserProfileRequest struct {
Username string `json:"username" validate:"required,username"`
}

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

type UpdateRoles struct {
Roles []string `json:"roles" validate:"required"`
}
Expand Down Expand Up @@ -181,6 +185,20 @@ func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error
return user, json.NewDecoder(res.Body).Decode(&user)
}

// UpdateUserPassword updates a user password.
// It calls PUT /users/{user}/password
func (c *Client) UpdateUserPassword(ctx context.Context, userID uuid.UUID, req UpdateUserPasswordRequest) error {
res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", uuidOrMe(userID)), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return readBodyAsError(res)
}
return nil
}

// UpdateUserRoles grants the userID the specified roles.
// Include ALL roles the user has.
func (c *Client) UpdateUserRoles(ctx context.Context, userID uuid.UUID, req UpdateRoles) (User, error) {
Expand Down
19 changes: 12 additions & 7 deletions site/src/api/typesGenerated.ts
Expand Up @@ -12,7 +12,7 @@ export interface AgentGitSSHKey {
readonly private_key: string
}

// From codersdk/users.go:105:6
// From codersdk/users.go:109: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:100:6
// From codersdk/users.go:104:6
export interface CreateOrganizationRequest {
readonly name: string
}
Expand Down Expand Up @@ -101,7 +101,7 @@ export interface CreateWorkspaceRequest {
readonly parameter_values: CreateParameterRequest[]
}

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

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

// From codersdk/users.go:91:6
// From codersdk/users.go:95:6
export interface LoginWithPasswordResponse {
readonly session_token: string
}
Expand Down Expand Up @@ -255,11 +255,16 @@ export interface UpdateActiveTemplateVersion {
readonly id: string
}

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

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

// From codersdk/users.go:70:6
export interface UpdateUserProfileRequest {
readonly email: string
Expand Down Expand Up @@ -291,7 +296,7 @@ export interface User {
readonly organization_ids: string[]
}

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

0 comments on commit 57bb108

Please sign in to comment.