Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 57bb108

Browse files
feat: Add update user password endpoint (#1310)
1 parent a2be7c0 commit 57bb108

File tree

12 files changed

+160
-26
lines changed

12 files changed

+160
-26
lines changed

coderd/coderd.go

+4
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ func New(options *Options) (http.Handler, func()) {
240240
r.Get("/", api.userByName)
241241
r.Put("/profile", api.putUserProfile)
242242
r.Put("/suspend", api.putUserSuspend)
243+
r.Route("/password", func(r chi.Router) {
244+
r.Use(httpmw.WithRBACObject(rbac.ResourceUserPasswordRole))
245+
r.Put("/", authorize(api.putUserPassword, rbac.ActionUpdate))
246+
})
243247
r.Get("/organizations", api.organizationsByUser)
244248
r.Post("/organizations", api.postOrganizationsByUser)
245249
// These roles apply to the site wide permissions.

coderd/coderdtest/coderdtest.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -174,21 +174,22 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
174174
return closer
175175
}
176176

177+
var FirstUserParams = codersdk.CreateFirstUserRequest{
178+
179+
Username: "testuser",
180+
Password: "testpass",
181+
OrganizationName: "testorg",
182+
}
183+
177184
// CreateFirstUser creates a user with preset credentials and authenticates
178185
// with the passed in codersdk client.
179186
func CreateFirstUser(t *testing.T, client *codersdk.Client) codersdk.CreateFirstUserResponse {
180-
req := codersdk.CreateFirstUserRequest{
181-
182-
Username: "testuser",
183-
Password: "testpass",
184-
OrganizationName: "testorg",
185-
}
186-
resp, err := client.CreateFirstUser(context.Background(), req)
187+
resp, err := client.CreateFirstUser(context.Background(), FirstUserParams)
187188
require.NoError(t, err)
188189

189190
login, err := client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
190-
Email: req.Email,
191-
Password: req.Password,
191+
Email: FirstUserParams.Email,
192+
Password: FirstUserParams.Password,
192193
})
193194
require.NoError(t, err)
194195
client.SessionToken = login.SessionToken

coderd/database/databasefake/databasefake.go

+15
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,21 @@ func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse
13141314
return database.User{}, sql.ErrNoRows
13151315
}
13161316

1317+
func (q *fakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error {
1318+
q.mutex.Lock()
1319+
defer q.mutex.Unlock()
1320+
1321+
for i, user := range q.users {
1322+
if user.ID != arg.ID {
1323+
continue
1324+
}
1325+
user.HashedPassword = arg.HashedPassword
1326+
q.users[i] = user
1327+
return nil
1328+
}
1329+
return sql.ErrNoRows
1330+
}
1331+
13171332
func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) {
13181333
q.mutex.Lock()
13191334
defer q.mutex.Unlock()

coderd/database/querier.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/users.sql

+9-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ WHERE
5959
id = @id
6060
RETURNING *;
6161

62+
-- name: UpdateUserHashedPassword :exec
63+
UPDATE
64+
users
65+
SET
66+
hashed_password = $2
67+
WHERE
68+
id = $1;
69+
6270
-- name: GetUsers :many
6371
SELECT
6472
*
@@ -133,4 +141,4 @@ FROM
133141
LEFT JOIN organization_members
134142
ON id = user_id
135143
WHERE
136-
id = @user_id;
144+
id = @user_id;

coderd/httpmw/userparam.go

-8
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,6 @@ func ExtractUserParam(db database.Store) func(http.Handler) http.Handler {
7676
}
7777
}
7878

79-
apiKey := APIKey(r)
80-
if apiKey.UserID != user.ID {
81-
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
82-
Message: "getting non-personal users isn't supported yet",
83-
})
84-
return
85-
}
86-
8779
ctx := context.WithValue(r.Context(), userParamContextKey{}, user)
8880
next.ServeHTTP(rw, r.WithContext(ctx))
8981
})

coderd/rbac/object.go

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ var (
2424
Type: "user_role",
2525
}
2626

27+
ResourceUserPasswordRole = Object{
28+
Type: "user_password",
29+
}
30+
2731
// ResourceWildcard represents all resource types
2832
ResourceWildcard = Object{
2933
Type: WildcardSymbol,

coderd/users.go

+30-1
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,36 @@ func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
360360
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
361361
}
362362

363+
func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {
364+
var (
365+
user = httpmw.UserParam(r)
366+
params codersdk.UpdateUserPasswordRequest
367+
)
368+
if !httpapi.Read(rw, r, &params) {
369+
return
370+
}
371+
372+
hashedPassword, err := userpassword.Hash(params.Password)
373+
if err != nil {
374+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
375+
Message: fmt.Sprintf("hash password: %s", err.Error()),
376+
})
377+
return
378+
}
379+
err = api.Database.UpdateUserHashedPassword(r.Context(), database.UpdateUserHashedPasswordParams{
380+
ID: user.ID,
381+
HashedPassword: []byte(hashedPassword),
382+
})
383+
if err != nil {
384+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
385+
Message: fmt.Sprintf("put user password: %s", err.Error()),
386+
})
387+
return
388+
}
389+
390+
httpapi.Write(rw, http.StatusNoContent, nil)
391+
}
392+
363393
func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) {
364394
user := httpmw.UserParam(r)
365395

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

579609
// If the user doesn't exist, it will be a default struct.
580-
581610
equal, err := userpassword.Compare(string(user.HashedPassword), loginWithPassword.Password)
582611
if err != nil {
583612
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{

coderd/users_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,44 @@ func TestUpdateUserProfile(t *testing.T) {
287287
})
288288
}
289289

290+
func TestUpdateUserPassword(t *testing.T) {
291+
t.Parallel()
292+
293+
t.Run("MemberCantUpdateAdminPassword", func(t *testing.T) {
294+
t.Parallel()
295+
client := coderdtest.New(t, nil)
296+
admin := coderdtest.CreateFirstUser(t, client)
297+
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
298+
err := member.UpdateUserPassword(context.Background(), admin.UserID, codersdk.UpdateUserPasswordRequest{
299+
Password: "newpassword",
300+
})
301+
require.Error(t, err, "member should not be able to update admin password")
302+
})
303+
304+
t.Run("AdminCanUpdateMemberPassword", func(t *testing.T) {
305+
t.Parallel()
306+
client := coderdtest.New(t, nil)
307+
admin := coderdtest.CreateFirstUser(t, client)
308+
member, err := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
309+
310+
Username: "coder",
311+
Password: "password",
312+
OrganizationID: admin.OrganizationID,
313+
})
314+
require.NoError(t, err, "create member")
315+
err = client.UpdateUserPassword(context.Background(), member.ID, codersdk.UpdateUserPasswordRequest{
316+
Password: "newpassword",
317+
})
318+
require.NoError(t, err, "admin should be able to update member password")
319+
// Check if the member can login using the new password
320+
_, err = client.LoginWithPassword(context.Background(), codersdk.LoginWithPasswordRequest{
321+
322+
Password: "newpassword",
323+
})
324+
require.NoError(t, err, "member should login successfully with the new password")
325+
})
326+
}
327+
290328
func TestGrantRoles(t *testing.T) {
291329
t.Parallel()
292330
t.Run("UpdateIncorrectRoles", func(t *testing.T) {

codersdk/users.go

+18
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ type UpdateUserProfileRequest struct {
7272
Username string `json:"username" validate:"required,username"`
7373
}
7474

75+
type UpdateUserPasswordRequest struct {
76+
Password string `json:"password" validate:"required"`
77+
}
78+
7579
type UpdateRoles struct {
7680
Roles []string `json:"roles" validate:"required"`
7781
}
@@ -181,6 +185,20 @@ func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error
181185
return user, json.NewDecoder(res.Body).Decode(&user)
182186
}
183187

188+
// UpdateUserPassword updates a user password.
189+
// It calls PUT /users/{user}/password
190+
func (c *Client) UpdateUserPassword(ctx context.Context, userID uuid.UUID, req UpdateUserPasswordRequest) error {
191+
res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", uuidOrMe(userID)), req)
192+
if err != nil {
193+
return err
194+
}
195+
defer res.Body.Close()
196+
if res.StatusCode != http.StatusNoContent {
197+
return readBodyAsError(res)
198+
}
199+
return nil
200+
}
201+
184202
// UpdateUserRoles grants the userID the specified roles.
185203
// Include ALL roles the user has.
186204
func (c *Client) UpdateUserRoles(ctx context.Context, userID uuid.UUID, req UpdateRoles) (User, error) {

site/src/api/typesGenerated.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface AgentGitSSHKey {
1212
readonly private_key: string
1313
}
1414

15-
// From codersdk/users.go:105:6
15+
// From codersdk/users.go:109:6
1616
export interface AuthMethods {
1717
readonly password: boolean
1818
readonly github: boolean
@@ -44,7 +44,7 @@ export interface CreateFirstUserResponse {
4444
readonly organization_id: string
4545
}
4646

47-
// From codersdk/users.go:100:6
47+
// From codersdk/users.go:104:6
4848
export interface CreateOrganizationRequest {
4949
readonly name: string
5050
}
@@ -101,7 +101,7 @@ export interface CreateWorkspaceRequest {
101101
readonly parameter_values: CreateParameterRequest[]
102102
}
103103

104-
// From codersdk/users.go:96:6
104+
// From codersdk/users.go:100:6
105105
export interface GenerateAPIKeyResponse {
106106
readonly key: string
107107
}
@@ -119,13 +119,13 @@ export interface GoogleInstanceIdentityToken {
119119
readonly json_web_token: string
120120
}
121121

122-
// From codersdk/users.go:85:6
122+
// From codersdk/users.go:89:6
123123
export interface LoginWithPasswordRequest {
124124
readonly email: string
125125
readonly password: string
126126
}
127127

128-
// From codersdk/users.go:91:6
128+
// From codersdk/users.go:95:6
129129
export interface LoginWithPasswordResponse {
130130
readonly session_token: string
131131
}
@@ -255,11 +255,16 @@ export interface UpdateActiveTemplateVersion {
255255
readonly id: string
256256
}
257257

258-
// From codersdk/users.go:75:6
258+
// From codersdk/users.go:79:6
259259
export interface UpdateRoles {
260260
readonly roles: string[]
261261
}
262262

263+
// From codersdk/users.go:75:6
264+
export interface UpdateUserPasswordRequest {
265+
readonly password: string
266+
}
267+
263268
// From codersdk/users.go:70:6
264269
export interface UpdateUserProfileRequest {
265270
readonly email: string
@@ -291,7 +296,7 @@ export interface User {
291296
readonly organization_ids: string[]
292297
}
293298

294-
// From codersdk/users.go:79:6
299+
// From codersdk/users.go:83:6
295300
export interface UserRoles {
296301
readonly roles: string[]
297302
readonly organization_roles: Record<string, string[]>

0 commit comments

Comments
 (0)