mirror of https://github.com/coder/coder.git
feat: Add update user password endpoint (#1310)
This commit is contained in:
parent
a2be7c0294
commit
57bb108465
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
id = @user_id;
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -24,6 +24,10 @@ var (
|
|||
Type: "user_role",
|
||||
}
|
||||
|
||||
ResourceUserPasswordRole = Object{
|
||||
Type: "user_password",
|
||||
}
|
||||
|
||||
// ResourceWildcard represents all resource types
|
||||
ResourceWildcard = Object{
|
||||
Type: WildcardSymbol,
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<string, string[]>
|
||||
|
|
Loading…
Reference in New Issue