feat: provide endpoint to lock/unlock workspace (#8239)

This commit is contained in:
Jon Ayers
2023-06-28 16:12:49 -05:00
committed by GitHub
parent 72e83df578
commit 749307ef08
31 changed files with 577 additions and 51 deletions
+21 -11
View File
@@ -143,13 +143,14 @@ var (
DisplayName: "Provisioner Daemon",
Site: rbac.Permissions(map[string][]rbac.Action{
// TODO: Add ProvisionerJob resource type.
rbac.ResourceFile.Type: {rbac.ActionRead},
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceUser.Type: {rbac.ActionRead},
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
rbac.ResourceUserData.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceAPIKey.Type: {rbac.WildcardSymbol},
rbac.ResourceFile.Type: {rbac.ActionRead},
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceUser.Type: {rbac.ActionRead},
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
rbac.ResourceUserData.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceAPIKey.Type: {rbac.WildcardSymbol},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
@@ -165,9 +166,10 @@ var (
Name: "autostart",
DisplayName: "Autostart Daemon",
Site: rbac.Permissions(map[string][]rbac.Action{
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate},
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
@@ -213,6 +215,7 @@ var (
rbac.ResourceUser.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
rbac.ResourceUserData.Type: {rbac.ActionCreate, rbac.ActionUpdate},
rbac.ResourceWorkspace.Type: {rbac.ActionUpdate},
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionUpdate},
rbac.ResourceWorkspaceExecution.Type: {rbac.ActionCreate},
rbac.ResourceWorkspaceProxy.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
}),
@@ -1998,7 +2001,7 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
action = rbac.ActionDelete
}
if err = q.authorizeContext(ctx, action, w); err != nil {
if err = q.authorizeContext(ctx, action, w.WorkspaceBuildRBAC(arg.Transition)); err != nil {
return database.WorkspaceBuild{}, err
}
@@ -2530,6 +2533,13 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg)
}
func (q *querier) UpdateWorkspaceLockedAt(ctx context.Context, arg database.UpdateWorkspaceLockedAtParams) error {
fetch := func(ctx context.Context, arg database.UpdateWorkspaceLockedAtParams) (database.Workspace, error) {
return q.db.GetWorkspaceByID(ctx, arg.ID)
}
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedAt)(ctx, arg)
}
func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
fetch := func(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
return q.db.GetWorkspaceProxyByID(ctx, arg.ID)
+2 -2
View File
@@ -1196,7 +1196,7 @@ func (s *MethodTestSuite) TestWorkspace() {
WorkspaceID: w.ID,
Transition: database.WorkspaceTransitionStart,
Reason: database.BuildReasonInitiator,
}).Asserts(w, rbac.ActionUpdate)
}).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate)
}))
s.Run("Delete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
w := dbgen.Workspace(s.T(), db, database.Workspace{})
@@ -1204,7 +1204,7 @@ func (s *MethodTestSuite) TestWorkspace() {
WorkspaceID: w.ID,
Transition: database.WorkspaceTransitionDelete,
Reason: database.BuildReasonInitiator,
}).Asserts(w, rbac.ActionDelete)
}).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionDelete), rbac.ActionDelete)
}))
s.Run("InsertWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) {
w := dbgen.Workspace(s.T(), db, database.Workspace{})
+20
View File
@@ -5197,6 +5197,26 @@ func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateWorkspaceLockedAt(_ context.Context, arg database.UpdateWorkspaceLockedAtParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for index, workspace := range q.workspaces {
if workspace.ID != arg.ID {
continue
}
workspace.LockedAt = arg.LockedAt
q.workspaces[index] = workspace
return nil
}
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
+7
View File
@@ -1531,6 +1531,13 @@ func (m metricsStore) UpdateWorkspaceLastUsedAt(ctx context.Context, arg databas
return err
}
func (m metricsStore) UpdateWorkspaceLockedAt(ctx context.Context, arg database.UpdateWorkspaceLockedAtParams) error {
start := time.Now()
r0 := m.s.UpdateWorkspaceLockedAt(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceLockedAt").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
start := time.Now()
proxy, err := m.s.UpdateWorkspaceProxy(ctx, arg)
+14
View File
@@ -3165,6 +3165,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceLastUsedAt(arg0, arg1 interface{
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLastUsedAt), arg0, arg1)
}
// UpdateWorkspaceLockedAt mocks base method.
func (m *MockStore) UpdateWorkspaceLockedAt(arg0 context.Context, arg1 database.UpdateWorkspaceLockedAtParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceLockedAt", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateWorkspaceLockedAt indicates an expected call of UpdateWorkspaceLockedAt.
func (mr *MockStoreMockRecorder) UpdateWorkspaceLockedAt(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceLockedAt", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceLockedAt), arg0, arg1)
}
// UpdateWorkspaceProxy mocks base method.
func (m *MockStore) UpdateWorkspaceProxy(arg0 context.Context, arg1 database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
m.ctrl.T.Helper()
+2 -1
View File
@@ -816,7 +816,8 @@ CREATE TABLE workspaces (
name character varying(64) NOT NULL,
autostart_schedule text,
ttl bigint,
last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL
last_used_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL,
locked_at timestamp with time zone
);
ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass);
@@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE workspaces DROP COLUMN locked_at;
COMMIT;
@@ -0,0 +1,3 @@
BEGIN;
ALTER TABLE workspaces ADD COLUMN locked_at timestamptz NULL;
COMMIT;
+33
View File
@@ -145,6 +145,11 @@ func (w Workspace) RBACObject() rbac.Object {
}
func (w Workspace) ExecutionRBAC() rbac.Object {
// If a workspace is locked it cannot be accessed.
if w.LockedAt.Valid {
return w.LockedRBAC()
}
return rbac.ResourceWorkspaceExecution.
WithID(w.ID).
InOrg(w.OrganizationID).
@@ -152,12 +157,40 @@ func (w Workspace) ExecutionRBAC() rbac.Object {
}
func (w Workspace) ApplicationConnectRBAC() rbac.Object {
// If a workspace is locked it cannot be accessed.
if w.LockedAt.Valid {
return w.LockedRBAC()
}
return rbac.ResourceWorkspaceApplicationConnect.
WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Object {
// If a workspace is locked it cannot be built.
// However we need to allow stopping a workspace by a caller once a workspace
// is locked (e.g. for autobuild). Additionally, if a user wants to delete
// a locked workspace, they shouldn't have to have it unlocked first.
if w.LockedAt.Valid && transition != WorkspaceTransitionStop &&
transition != WorkspaceTransitionDelete {
return w.LockedRBAC()
}
return rbac.ResourceWorkspaceBuild.
WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
func (w Workspace) LockedRBAC() rbac.Object {
return rbac.ResourceWorkspaceLocked.
WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
func (m OrganizationMember) RBACObject() rbac.Object {
return rbac.ResourceOrganizationMember.
WithID(m.UserID).
+1
View File
@@ -235,6 +235,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
&i.Count,
); err != nil {
return nil, err
+1
View File
@@ -1687,6 +1687,7 @@ type Workspace struct {
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
}
type WorkspaceAgent struct {
+1
View File
@@ -254,6 +254,7 @@ type sqlcQuerier interface {
UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) (WorkspaceBuild, error)
UpdateWorkspaceDeletedByID(ctx context.Context, arg UpdateWorkspaceDeletedByIDParams) error
UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWorkspaceLastUsedAtParams) error
UpdateWorkspaceLockedAt(ctx context.Context, arg UpdateWorkspaceLockedAtParams) error
// This allows editing the properties of a workspace proxy.
UpdateWorkspaceProxy(ctx context.Context, arg UpdateWorkspaceProxyParams) (WorkspaceProxy, error)
UpdateWorkspaceProxyDeleted(ctx context.Context, arg UpdateWorkspaceProxyDeletedParams) error
+36 -8
View File
@@ -8147,7 +8147,7 @@ func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploy
const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
FROM
workspaces
WHERE
@@ -8190,13 +8190,14 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
)
return i, err
}
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
FROM
workspaces
WHERE
@@ -8220,13 +8221,14 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
)
return i, err
}
const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
FROM
workspaces
WHERE
@@ -8257,13 +8259,14 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
)
return i, err
}
const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
FROM
workspaces
WHERE
@@ -8313,13 +8316,14 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
)
return i, err
}
const getWorkspaces = `-- name: GetWorkspaces :many
SELECT
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, COUNT(*) OVER () as count
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at, COUNT(*) OVER () as count
FROM
workspaces
JOIN
@@ -8529,6 +8533,7 @@ type GetWorkspacesRow struct {
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
Count int64 `db:"count" json:"count"`
}
@@ -8565,6 +8570,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
&i.Count,
); err != nil {
return nil, err
@@ -8582,7 +8588,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many
SELECT
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.locked_at
FROM
workspaces
LEFT JOIN
@@ -8651,6 +8657,7 @@ func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
); err != nil {
return nil, err
}
@@ -8680,7 +8687,7 @@ INSERT INTO
last_used_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
`
type InsertWorkspaceParams struct {
@@ -8722,6 +8729,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
)
return i, err
}
@@ -8734,7 +8742,7 @@ SET
WHERE
id = $1
AND deleted = false
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, locked_at
`
type UpdateWorkspaceParams struct {
@@ -8757,6 +8765,7 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
&i.LockedAt,
)
return i, err
}
@@ -8818,6 +8827,25 @@ func (q *sqlQuerier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg UpdateWo
return err
}
const updateWorkspaceLockedAt = `-- name: UpdateWorkspaceLockedAt :exec
UPDATE
workspaces
SET
locked_at = $2
WHERE
id = $1
`
type UpdateWorkspaceLockedAtParams struct {
ID uuid.UUID `db:"id" json:"id"`
LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
}
func (q *sqlQuerier) UpdateWorkspaceLockedAt(ctx context.Context, arg UpdateWorkspaceLockedAtParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceLockedAt, arg.ID, arg.LockedAt)
return err
}
const updateWorkspaceTTL = `-- name: UpdateWorkspaceTTL :exec
UPDATE
workspaces
+8
View File
@@ -453,3 +453,11 @@ WHERE
workspace_builds.transition = 'start'::workspace_transition
)
) AND workspaces.deleted = 'false';
-- name: UpdateWorkspaceLockedAt :exec
UPDATE
workspaces
SET
locked_at = $2
WHERE
id = $1;