Skip to content

Commit e05480d

Browse files
committed
feat: POC for allowing TemplateAdmin to delete prebuild workspaces via auth layer
1 parent af4a668 commit e05480d

File tree

10 files changed

+181
-8
lines changed

10 files changed

+181
-8
lines changed

coderd/database/dbauthz/dbauthz.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,9 @@ var (
412412
policy.ActionCreate, policy.ActionDelete, policy.ActionRead, policy.ActionUpdate,
413413
policy.ActionWorkspaceStart, policy.ActionWorkspaceStop,
414414
},
415+
rbac.ResourcePrebuiltWorkspace.Type: {
416+
policy.ActionRead, policy.ActionUpdate, policy.ActionDelete,
417+
},
415418
// Should be able to add the prebuilds system user as a member to any organization that needs prebuilds.
416419
rbac.ResourceOrganizationMember.Type: {
417420
policy.ActionCreate,
@@ -3909,7 +3912,11 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
39093912
action = policy.ActionWorkspaceStop
39103913
}
39113914

3912-
if err = q.authorizeContext(ctx, action, w); err != nil {
3915+
if action == policy.ActionDelete && w.IsPrebuild() {
3916+
if err := q.authorizeContext(ctx, action, w.PrebuildRBAC()); err != nil {
3917+
return xerrors.Errorf("authorize context: %w", err)
3918+
}
3919+
} else if err = q.authorizeContext(ctx, action, w); err != nil {
39133920
return xerrors.Errorf("authorize context: %w", err)
39143921
}
39153922

@@ -3927,7 +3934,11 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
39273934
// to use a non-active version then we must fail the request.
39283935
if accessControl.RequireActiveVersion {
39293936
if arg.TemplateVersionID != t.ActiveVersionID {
3930-
if err = q.authorizeContext(ctx, policy.ActionUpdate, t); err != nil {
3937+
if w.IsPrebuild() {
3938+
if err := q.authorizeContext(ctx, policy.ActionUpdate, w.PrebuildRBAC()); err != nil {
3939+
return xerrors.Errorf("cannot use non-active version: %w", err)
3940+
}
3941+
} else if err = q.authorizeContext(ctx, policy.ActionUpdate, t); err != nil {
39313942
return xerrors.Errorf("cannot use non-active version: %w", err)
39323943
}
39333944
}
@@ -3949,7 +3960,11 @@ func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg databa
39493960
return err
39503961
}
39513962

3952-
err = q.authorizeContext(ctx, policy.ActionUpdate, workspace)
3963+
if workspace.IsPrebuild() {
3964+
err = q.authorizeContext(ctx, policy.ActionUpdate, workspace.PrebuildRBAC())
3965+
} else {
3966+
err = q.authorizeContext(ctx, policy.ActionUpdate, workspace)
3967+
}
39533968
if err != nil {
39543969
return err
39553970
}

coderd/database/modelmethods.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,26 @@ func (w Workspace) WorkspaceTable() WorkspaceTable {
226226
}
227227

228228
func (w Workspace) RBACObject() rbac.Object {
229+
//if w.IsPrebuild() {
230+
// return w.PrebuildRBAC()
231+
//}
229232
return w.WorkspaceTable().RBACObject()
230233
}
231234

235+
func (w Workspace) IsPrebuild() bool {
236+
// TODO: avoid import cycle
237+
return w.OwnerID == uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
238+
}
239+
240+
func (w Workspace) PrebuildRBAC() rbac.Object {
241+
if w.IsPrebuild() {
242+
return rbac.ResourcePrebuiltWorkspace.WithID(w.ID).
243+
InOrg(w.OrganizationID).
244+
WithOwner(w.OwnerID.String())
245+
}
246+
return w.RBACObject()
247+
}
248+
232249
func (w WorkspaceTable) RBACObject() rbac.Object {
233250
if w.DormantAt.Valid {
234251
return w.DormantRBAC()

coderd/rbac/object_gen.go

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

coderd/rbac/policy/policy.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ var RBACPermissions = map[string]PermissionDefinition{
102102
"workspace_dormant": {
103103
Actions: workspaceActions,
104104
},
105+
"prebuilt_workspace": {
106+
Actions: map[Action]ActionDefinition{
107+
ActionRead: actDef("read prebuilt workspace"),
108+
ActionUpdate: actDef("update prebuilt workspace"),
109+
ActionDelete: actDef("delete prebuilt workspace"),
110+
},
111+
},
105112
"workspace_proxy": {
106113
Actions: map[Action]ActionDefinition{
107114
ActionCreate: actDef("create a workspace proxy"),

coderd/rbac/roles.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
335335
ResourceAssignOrgRole.Type: {policy.ActionRead},
336336
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
337337
// CRUD all files, even those they did not upload.
338-
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
339-
ResourceWorkspace.Type: {policy.ActionRead},
338+
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
339+
ResourceWorkspace.Type: {policy.ActionRead},
340+
ResourcePrebuiltWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
340341
// CRUD to provisioner daemons for now.
341342
ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
342343
// Needs to read all organizations since
@@ -493,9 +494,10 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
493494
Site: []Permission{},
494495
Org: map[string][]Permission{
495496
organizationID.String(): Permissions(map[string][]policy.Action{
496-
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
497-
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
498-
ResourceWorkspace.Type: {policy.ActionRead},
497+
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
498+
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
499+
ResourceWorkspace.Type: {policy.ActionRead},
500+
ResourcePrebuiltWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
499501
// Assigning template perms requires this permission.
500502
ResourceOrganization.Type: {policy.ActionRead},
501503
ResourceOrganizationMember.Type: {policy.ActionRead},

coderd/workspacebuilds.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,11 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
404404
ctx,
405405
tx,
406406
func(action policy.Action, object rbac.Objecter) bool {
407+
if object.RBACObject().Type == rbac.ResourceWorkspace.Type && action == policy.ActionDelete {
408+
workspaceObj := object.(database.Workspace)
409+
prebuild := workspaceObj.PrebuildRBAC()
410+
return api.Authorize(r, action, prebuild)
411+
}
407412
return api.Authorize(r, action, object)
408413
},
409414
audit.WorkspaceBuildBaggageFromRequest(r),

codersdk/rbacresources_gen.go

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

enterprise/coderd/prebuilds/reconcile_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import (
88
"testing"
99
"time"
1010

11+
"github.com/coder/coder/v2/coderd/coderdtest"
12+
"github.com/coder/coder/v2/coderd/rbac"
13+
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
14+
"github.com/coder/coder/v2/enterprise/coderd/license"
15+
"github.com/coder/coder/v2/provisionersdk"
16+
1117
"github.com/prometheus/client_golang/prometheus"
1218
"github.com/stretchr/testify/assert"
1319
"golang.org/x/xerrors"
@@ -420,6 +426,108 @@ func TestPrebuildReconciliation(t *testing.T) {
420426
}
421427
}
422428

429+
func TestTemplateAdminDelete(t *testing.T) {
430+
t.Parallel()
431+
432+
if !dbtestutil.WillUsePostgres() {
433+
t.Skip("This test requires postgres")
434+
}
435+
436+
t.Run("template admin delete prebuilds", func(t *testing.T) {
437+
t.Parallel()
438+
439+
clock := quartz.NewMock(t)
440+
441+
// Setup.
442+
ctx := testutil.Context(t, testutil.WaitSuperLong)
443+
db, pubsub := dbtestutil.NewDB(t)
444+
445+
spy := newStoreSpy(db, nil)
446+
447+
logger := testutil.Logger(t)
448+
client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
449+
Options: &coderdtest.Options{
450+
Database: spy,
451+
Pubsub: pubsub,
452+
},
453+
LicenseOptions: &coderdenttest.LicenseOptions{
454+
Features: license.Features{
455+
codersdk.FeatureExternalProvisionerDaemons: 1,
456+
},
457+
},
458+
459+
EntitlementsUpdateInterval: time.Second,
460+
})
461+
462+
orgID := owner.OrganizationID
463+
464+
provisionerCloser := coderdenttest.NewExternalProvisionerDaemon(t, client, orgID, map[string]string{
465+
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
466+
})
467+
defer provisionerCloser.Close()
468+
469+
reconciler := prebuilds.NewStoreReconciler(spy, pubsub, codersdk.PrebuildsConfig{}, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer())
470+
var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(spy)
471+
api.AGPL.PrebuildsClaimer.Store(&claimer)
472+
473+
version := coderdtest.CreateTemplateVersion(t, client, orgID, templateWithAgentAndPresetsWithPrebuilds(2))
474+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
475+
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
476+
presets, err := client.TemplateVersionPresets(ctx, version.ID)
477+
require.NoError(t, err)
478+
require.Len(t, presets, 1)
479+
preset := setupTestDBPreset(t, db, version.ID, 2, "b0rked")
480+
481+
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.RoleTemplateAdmin())
482+
483+
state, err := reconciler.SnapshotState(ctx, spy)
484+
require.NoError(t, err)
485+
require.Len(t, state.Presets, 2)
486+
487+
for _, preset := range presets {
488+
ps, err := state.FilterByPreset(preset.ID)
489+
require.NoError(t, err)
490+
require.NotNil(t, ps)
491+
actions, err := reconciler.CalculateActions(ctx, *ps)
492+
require.NoError(t, err)
493+
require.NotNil(t, actions)
494+
495+
require.NoError(t, reconciler.ReconcilePreset(ctx, *ps))
496+
}
497+
498+
workspace, _ := setupTestDBPrebuild(
499+
t,
500+
clock,
501+
db,
502+
pubsub,
503+
database.WorkspaceTransitionStart,
504+
database.ProvisionerJobStatusSucceeded,
505+
orgID,
506+
preset,
507+
template.ID,
508+
version.ID,
509+
)
510+
511+
require.NoError(t, reconciler.ReconcileAll(ctx))
512+
513+
runningWorkspaces, err := db.GetRunningPrebuiltWorkspaces(ctx)
514+
require.NoError(t, err)
515+
516+
prebuiltWorkspace, err := db.GetWorkspaceByID(ctx, runningWorkspaces[0].ID)
517+
require.NoError(t, err)
518+
519+
build, err := templateAdminClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
520+
Transition: codersdk.WorkspaceTransitionDelete,
521+
})
522+
require.NoError(t, err, "delete the workspace")
523+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
524+
525+
workspaceNew, err := client.DeletedWorkspace(ctx, prebuiltWorkspace.ID)
526+
require.NoError(t, err)
527+
require.Equal(t, prebuiltWorkspace.ID, workspaceNew.ID)
528+
})
529+
}
530+
423531
// brokenPublisher is used to validate that Publish() calls which always fail do not affect the reconciler's behavior,
424532
// since the messages published are not essential but merely advisory.
425533
type brokenPublisher struct {

site/src/api/rbacresourcesGenerated.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ export const RBACResourceActions: Partial<
123123
read: "read member",
124124
update: "update an organization member",
125125
},
126+
prebuilt_workspace: {
127+
delete: "delete prebuilt workspace",
128+
read: "read prebuilt workspace",
129+
update: "update prebuilt workspace",
130+
},
126131
provisioner_daemon: {
127132
create: "create a provisioner daemon/key",
128133
delete: "delete a provisioner daemon/key",

site/src/api/typesGenerated.ts

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

0 commit comments

Comments
 (0)