feat: add PUT /api/v2/users/:user-id/suspend endpoint (#1154)

This commit is contained in:
Bruno Quaresma
2022-04-26 09:00:07 -05:00
committed by GitHub
parent f9ce54a51e
commit 441ffd6a0b
14 changed files with 202 additions and 25 deletions

View File

@ -41,6 +41,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
"hashed_password": ActionSecret, // A user can change their own password.
"created_at": ActionIgnore, // Never changes.
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
"status": ActionTrack, // A user can update another user status
},
&database.Workspace{}: {
"id": ActionIgnore, // Never changes.

View File

@ -182,6 +182,7 @@ func New(options *Options) (http.Handler, func()) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/", api.userByName)
r.Put("/profile", api.putUserProfile)
r.Put("/suspend", api.putUserSuspend)
r.Get("/organizations", api.organizationsByUser)
r.Post("/organizations", api.postOrganizationsByUser)
r.Post("/keys", api.postAPIKey)

View File

@ -1138,6 +1138,7 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
CreatedAt: arg.CreatedAt,
UpdatedAt: arg.UpdatedAt,
Username: arg.Username,
Status: database.UserStatusActive,
}
q.users = append(q.users, user)
return user, nil
@ -1159,6 +1160,22 @@ func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUs
return database.User{}, sql.ErrNoRows
}
func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for index, user := range q.users {
if user.ID != arg.ID {
continue
}
user.Status = arg.Status
user.UpdatedAt = arg.UpdatedAt
q.users[index] = user
return user, nil
}
return database.User{}, sql.ErrNoRows
}
func (q *fakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWorkspaceParams) (database.Workspace, error) {
q.mutex.Lock()
defer q.mutex.Unlock()

View File

@ -56,6 +56,11 @@ CREATE TYPE provisioner_type AS ENUM (
'terraform'
);
CREATE TYPE user_status AS ENUM (
'active',
'suspended'
);
CREATE TYPE workspace_transition AS ENUM (
'start',
'stop',
@ -221,7 +226,8 @@ CREATE TABLE users (
username text DEFAULT ''::text NOT NULL,
hashed_password bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
updated_at timestamp with time zone NOT NULL,
status user_status DEFAULT 'active'::public.user_status NOT NULL
);
CREATE TABLE workspace_agents (

View File

@ -0,0 +1,4 @@
ALTER TABLE ONLY users
DROP COLUMN IF EXISTS status;
DROP TYPE user_status;

View File

@ -0,0 +1,4 @@
CREATE TYPE user_status AS ENUM ('active', 'suspended');
ALTER TABLE ONLY users
ADD COLUMN IF NOT EXISTS status user_status NOT NULL DEFAULT 'active';

View File

@ -208,6 +208,25 @@ func (e *ProvisionerType) Scan(src interface{}) error {
return nil
}
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusSuspended UserStatus = "suspended"
)
func (e *UserStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = UserStatus(s)
case string:
*e = UserStatus(s)
default:
return fmt.Errorf("unsupported scan type for UserStatus: %T", src)
}
return nil
}
type WorkspaceTransition string
const (
@ -372,12 +391,13 @@ type TemplateVersion struct {
}
type User struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Status UserStatus `db:"status" json:"status"`
}
type Workspace struct {

View File

@ -85,6 +85,7 @@ type querier interface {
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error)
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error)
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
UpdateWorkspaceAutostop(ctx context.Context, arg UpdateWorkspaceAutostopParams) error

View File

@ -1782,7 +1782,7 @@ func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTe
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT
id, email, username, hashed_password, created_at, updated_at
id, email, username, hashed_password, created_at, updated_at, status
FROM
users
WHERE
@ -1807,13 +1807,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT
id, email, username, hashed_password, created_at, updated_at
id, email, username, hashed_password, created_at, updated_at, status
FROM
users
WHERE
@ -1832,6 +1833,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
)
return i, err
}
@ -1852,7 +1854,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
const getUsers = `-- name: GetUsers :many
SELECT
id, email, username, hashed_password, created_at, updated_at
id, email, username, hashed_password, created_at, updated_at, status
FROM
users
WHERE
@ -1922,6 +1924,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
); err != nil {
return nil, err
}
@ -1947,7 +1950,7 @@ INSERT INTO
updated_at
)
VALUES
($1, $2, $3, $4, $5, $6) RETURNING id, email, username, hashed_password, created_at, updated_at
($1, $2, $3, $4, $5, $6) RETURNING id, email, username, hashed_password, created_at, updated_at, status
`
type InsertUserParams struct {
@ -1976,6 +1979,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
)
return i, err
}
@ -1988,7 +1992,7 @@ SET
username = $3,
updated_at = $4
WHERE
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status
`
type UpdateUserProfileParams struct {
@ -2013,6 +2017,38 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
)
return i, err
}
const updateUserStatus = `-- name: UpdateUserStatus :one
UPDATE
users
SET
status = $2,
updated_at = $3
WHERE
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status
`
type UpdateUserStatusParams struct {
ID uuid.UUID `db:"id" json:"id"`
Status UserStatus `db:"status" json:"status"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserStatus, arg.ID, arg.Status, arg.UpdatedAt)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
)
return i, err
}

View File

@ -90,3 +90,12 @@ ORDER BY
LIMIT
-- A null limit means "no limit", so -1 means return all
NULLIF(@limit_opt :: int, -1);
-- name: UpdateUserStatus :one
UPDATE
users
SET
status = $2,
updated_at = $3
WHERE
id = $1 RETURNING *;

View File

@ -281,6 +281,25 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile))
}
func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
suspendedUser, err := api.Database.UpdateUserStatus(r.Context(), database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusSuspended,
UpdatedAt: database.Now(),
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("put user suspended: %s", err.Error()),
})
return
}
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser))
}
// Returns organizations the parameterized user has access to.
func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
@ -613,6 +632,7 @@ func convertUser(user database.User) codersdk.User {
Email: user.Email,
CreatedAt: user.CreatedAt,
Username: user.Username,
Status: codersdk.UserStatus(user.Status),
}
}

View File

@ -286,6 +286,38 @@ func TestUpdateUserProfile(t *testing.T) {
})
}
func TestPutUserSuspend(t *testing.T) {
t.Parallel()
t.Run("SuspendAnotherUser", func(t *testing.T) {
t.Skip()
t.Parallel()
client := coderdtest.New(t, nil)
me := coderdtest.CreateFirstUser(t, client)
client.User(context.Background(), codersdk.Me)
user, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "bruno@coder.com",
Username: "bruno",
Password: "password",
OrganizationID: me.OrganizationID,
})
user, err := client.SuspendUser(context.Background(), user.ID)
require.NoError(t, err)
require.Equal(t, user.Status, codersdk.UserStatusSuspended)
})
t.Run("SuspendItSelf", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
client.User(context.Background(), codersdk.Me)
suspendedUser, err := client.SuspendUser(context.Background(), codersdk.Me)
require.NoError(t, err)
require.Equal(t, suspendedUser.Status, codersdk.UserStatusSuspended)
})
}
func TestUserByName(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)