feat: Add update user password endpoint (#1310)

This commit is contained in:
Bruno Quaresma 2022-05-06 09:20:08 -05:00 committed by GitHub
parent a2be7c0294
commit 57bb108465
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 160 additions and 26 deletions

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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;

View File

@ -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))
})

View File

@ -24,6 +24,10 @@ var (
Type: "user_role",
}
ResourceUserPasswordRole = Object{
Type: "user_password",
}
// ResourceWildcard represents all resource types
ResourceWildcard = Object{
Type: WildcardSymbol,

View File

@ -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)
@ -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{

View File

@ -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) {

View File

@ -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) {

View File

@ -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[]>