diff --git a/coderd/coderd.go b/coderd/coderd.go index 191d3339aa2b1..aa5cdbb21b86c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -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. diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 074767f35920c..e205e272db3ce 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -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 diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 5726c9f7befe5..a9c7dea5fdbd5 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -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() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index db2f3f0d49987..dbf6b5cfed8cd 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -100,6 +100,7 @@ type querier interface { UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error + UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f91070915c097..450d540a192ac 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2264,6 +2264,25 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User return i, err } +const updateUserHashedPassword = `-- name: UpdateUserHashedPassword :exec +UPDATE + users +SET + hashed_password = $2 +WHERE + id = $1 +` + +type UpdateUserHashedPasswordParams struct { + ID uuid.UUID `db:"id" json:"id"` + HashedPassword []byte `db:"hashed_password" json:"hashed_password"` +} + +func (q *sqlQuerier) UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error { + _, err := q.db.ExecContext(ctx, updateUserHashedPassword, arg.ID, arg.HashedPassword) + return err +} + const updateUserProfile = `-- name: UpdateUserProfile :one UPDATE users diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 2f0ecb3709b9b..d0c3d4c3e7b66 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -59,6 +59,14 @@ WHERE id = @id RETURNING *; +-- name: UpdateUserHashedPassword :exec +UPDATE + users +SET + hashed_password = $2 +WHERE + id = $1; + -- name: GetUsers :many SELECT * @@ -133,4 +141,4 @@ FROM LEFT JOIN organization_members ON id = user_id WHERE - id = @user_id; \ No newline at end of file + id = @user_id; diff --git a/coderd/httpmw/userparam.go b/coderd/httpmw/userparam.go index ba91414de8f4c..16e768c4ba8c8 100644 --- a/coderd/httpmw/userparam.go +++ b/coderd/httpmw/userparam.go @@ -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)) }) diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index a4be9b1edab5b..dd12efc25fcb2 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -24,6 +24,10 @@ var ( Type: "user_role", } + ResourceUserPasswordRole = Object{ + Type: "user_password", + } + // ResourceWildcard represents all resource types ResourceWildcard = Object{ Type: WildcardSymbol, diff --git a/coderd/users.go b/coderd/users.go index 310bee8e79597..108ad0813cb49 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -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, ¶ms) { + 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) @@ -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{ diff --git a/coderd/users_test.go b/coderd/users_test.go index 531ac93be7aa6..aaf5737ce6b61 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -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) { diff --git a/codersdk/users.go b/codersdk/users.go index e2c8261febdec..dd0ad207d91e3 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -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"` } @@ -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) { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ee4add928bb11..e3fa4e53fc0d5 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:105:6 +// From codersdk/users.go:109: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:100:6 +// From codersdk/users.go:104:6 export interface CreateOrganizationRequest { readonly name: string } @@ -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 } @@ -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 } @@ -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 @@ -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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy