Skip to content

Commit 749307e

Browse files
authored
feat: provide endpoint to lock/unlock workspace (coder#8239)
1 parent 72e83df commit 749307e

31 files changed

+577
-51
lines changed

cli/testdata/coder_list_--output_json.golden

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"autostart_schedule": "CRON_TZ=US/Central 30 9 * * 1-5",
5252
"ttl_ms": 28800000,
5353
"last_used_at": "[timestamp]",
54-
"deleting_at": null
54+
"deleting_at": null,
55+
"locked_at": null
5556
}
5657
]

coderd/apidoc/docs.go

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ func New(options *Options) *API {
735735
})
736736
r.Get("/watch", api.watchWorkspace)
737737
r.Put("/extend", api.putExtendWorkspace)
738+
r.Put("/lock", api.putWorkspaceLock)
738739
})
739740
})
740741
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {

coderd/database/dbauthz/dbauthz.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,14 @@ var (
143143
DisplayName: "Provisioner Daemon",
144144
Site: rbac.Permissions(map[string][]rbac.Action{
145145
// TODO: Add ProvisionerJob resource type.
146-
rbac.ResourceFile.Type: {rbac.ActionRead},
147-
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
148-
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
149-
rbac.ResourceUser.Type: {rbac.ActionRead},
150-
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
151-
rbac.ResourceUserData.Type: {rbac.ActionRead, rbac.ActionUpdate},
152-
rbac.ResourceAPIKey.Type: {rbac.WildcardSymbol},
146+
rbac.ResourceFile.Type: {rbac.ActionRead},
147+
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
148+
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
149+
rbac.ResourceUser.Type: {rbac.ActionRead},
150+
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
151+
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
152+
rbac.ResourceUserData.Type: {rbac.ActionRead, rbac.ActionUpdate},
153+
rbac.ResourceAPIKey.Type: {rbac.WildcardSymbol},
153154
}),
154155
Org: map[string][]rbac.Permission{},
155156
User: []rbac.Permission{},
@@ -165,9 +166,10 @@ var (
165166
Name: "autostart",
166167
DisplayName: "Autostart Daemon",
167168
Site: rbac.Permissions(map[string][]rbac.Action{
168-
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
169-
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
170-
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate},
169+
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
170+
rbac.ResourceTemplate.Type: {rbac.ActionRead, rbac.ActionUpdate},
171+
rbac.ResourceWorkspace.Type: {rbac.ActionRead, rbac.ActionUpdate},
172+
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
171173
}),
172174
Org: map[string][]rbac.Permission{},
173175
User: []rbac.Permission{},
@@ -213,6 +215,7 @@ var (
213215
rbac.ResourceUser.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
214216
rbac.ResourceUserData.Type: {rbac.ActionCreate, rbac.ActionUpdate},
215217
rbac.ResourceWorkspace.Type: {rbac.ActionUpdate},
218+
rbac.ResourceWorkspaceBuild.Type: {rbac.ActionUpdate},
216219
rbac.ResourceWorkspaceExecution.Type: {rbac.ActionCreate},
217220
rbac.ResourceWorkspaceProxy.Type: {rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
218221
}),
@@ -1998,7 +2001,7 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
19982001
action = rbac.ActionDelete
19992002
}
20002003

2001-
if err = q.authorizeContext(ctx, action, w); err != nil {
2004+
if err = q.authorizeContext(ctx, action, w.WorkspaceBuildRBAC(arg.Transition)); err != nil {
20022005
return database.WorkspaceBuild{}, err
20032006
}
20042007

@@ -2530,6 +2533,13 @@ func (q *querier) UpdateWorkspaceLastUsedAt(ctx context.Context, arg database.Up
25302533
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLastUsedAt)(ctx, arg)
25312534
}
25322535

2536+
func (q *querier) UpdateWorkspaceLockedAt(ctx context.Context, arg database.UpdateWorkspaceLockedAtParams) error {
2537+
fetch := func(ctx context.Context, arg database.UpdateWorkspaceLockedAtParams) (database.Workspace, error) {
2538+
return q.db.GetWorkspaceByID(ctx, arg.ID)
2539+
}
2540+
return update(q.log, q.auth, fetch, q.db.UpdateWorkspaceLockedAt)(ctx, arg)
2541+
}
2542+
25332543
func (q *querier) UpdateWorkspaceProxy(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
25342544
fetch := func(ctx context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
25352545
return q.db.GetWorkspaceProxyByID(ctx, arg.ID)

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,15 +1196,15 @@ func (s *MethodTestSuite) TestWorkspace() {
11961196
WorkspaceID: w.ID,
11971197
Transition: database.WorkspaceTransitionStart,
11981198
Reason: database.BuildReasonInitiator,
1199-
}).Asserts(w, rbac.ActionUpdate)
1199+
}).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate)
12001200
}))
12011201
s.Run("Delete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
12021202
w := dbgen.Workspace(s.T(), db, database.Workspace{})
12031203
check.Args(database.InsertWorkspaceBuildParams{
12041204
WorkspaceID: w.ID,
12051205
Transition: database.WorkspaceTransitionDelete,
12061206
Reason: database.BuildReasonInitiator,
1207-
}).Asserts(w, rbac.ActionDelete)
1207+
}).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionDelete), rbac.ActionDelete)
12081208
}))
12091209
s.Run("InsertWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) {
12101210
w := dbgen.Workspace(s.T(), db, database.Workspace{})

coderd/database/dbfake/dbfake.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5197,6 +5197,26 @@ func (q *fakeQuerier) UpdateWorkspaceLastUsedAt(_ context.Context, arg database.
51975197
return sql.ErrNoRows
51985198
}
51995199

5200+
func (q *fakeQuerier) UpdateWorkspaceLockedAt(_ context.Context, arg database.UpdateWorkspaceLockedAtParams) error {
5201+
if err := validateDatabaseType(arg); err != nil {
5202+
return err
5203+
}
5204+
5205+
q.mutex.Lock()
5206+
defer q.mutex.Unlock()
5207+
5208+
for index, workspace := range q.workspaces {
5209+
if workspace.ID != arg.ID {
5210+
continue
5211+
}
5212+
workspace.LockedAt = arg.LockedAt
5213+
q.workspaces[index] = workspace
5214+
return nil
5215+
}
5216+
5217+
return sql.ErrNoRows
5218+
}
5219+
52005220
func (q *fakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.UpdateWorkspaceProxyParams) (database.WorkspaceProxy, error) {
52015221
q.mutex.Lock()
52025222
defer q.mutex.Unlock()

coderd/database/dbmetrics/dbmetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dump.sql

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
BEGIN;
2+
ALTER TABLE workspaces DROP COLUMN locked_at;
3+
COMMIT;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
BEGIN;
2+
ALTER TABLE workspaces ADD COLUMN locked_at timestamptz NULL;
3+
COMMIT;

coderd/database/modelmethods.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,19 +145,52 @@ func (w Workspace) RBACObject() rbac.Object {
145145
}
146146

147147
func (w Workspace) ExecutionRBAC() rbac.Object {
148+
// If a workspace is locked it cannot be accessed.
149+
if w.LockedAt.Valid {
150+
return w.LockedRBAC()
151+
}
152+
148153
return rbac.ResourceWorkspaceExecution.
149154
WithID(w.ID).
150155
InOrg(w.OrganizationID).
151156
WithOwner(w.OwnerID.String())
152157
}
153158

154159
func (w Workspace) ApplicationConnectRBAC() rbac.Object {
160+
// If a workspace is locked it cannot be accessed.
161+
if w.LockedAt.Valid {
162+
return w.LockedRBAC()
163+
}
164+
155165
return rbac.ResourceWorkspaceApplicationConnect.
156166
WithID(w.ID).
157167
InOrg(w.OrganizationID).
158168
WithOwner(w.OwnerID.String())
159169
}
160170

171+
func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Object {
172+
// If a workspace is locked it cannot be built.
173+
// However we need to allow stopping a workspace by a caller once a workspace
174+
// is locked (e.g. for autobuild). Additionally, if a user wants to delete
175+
// a locked workspace, they shouldn't have to have it unlocked first.
176+
if w.LockedAt.Valid && transition != WorkspaceTransitionStop &&
177+
transition != WorkspaceTransitionDelete {
178+
return w.LockedRBAC()
179+
}
180+
181+
return rbac.ResourceWorkspaceBuild.
182+
WithID(w.ID).
183+
InOrg(w.OrganizationID).
184+
WithOwner(w.OwnerID.String())
185+
}
186+
187+
func (w Workspace) LockedRBAC() rbac.Object {
188+
return rbac.ResourceWorkspaceLocked.
189+
WithID(w.ID).
190+
InOrg(w.OrganizationID).
191+
WithOwner(w.OwnerID.String())
192+
}
193+
161194
func (m OrganizationMember) RBACObject() rbac.Object {
162195
return rbac.ResourceOrganizationMember.
163196
WithID(m.UserID).

coderd/database/modelqueries.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
235235
&i.AutostartSchedule,
236236
&i.Ttl,
237237
&i.LastUsedAt,
238+
&i.LockedAt,
238239
&i.Count,
239240
); err != nil {
240241
return nil, err

0 commit comments

Comments
 (0)