8000 feat: allow TemplateAdmin to delete prebuilds via auth layer by ssncferreira · Pull Request #18333 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

feat: allow TemplateAdmin to delete prebuilds via auth layer #18333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 20 additions & 2 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,9 @@ var (
policy.ActionCreate, policy.ActionDelete, policy.ActionRead, policy.ActionUpdate,
policy.ActionWorkspaceStart, policy.ActionWorkspaceStop,
},
rbac.ResourcePrebuiltWorkspace.Type: {
policy.ActionRead, policy.ActionUpdate, policy.ActionDelete,
},
// Should be able to add the prebuilds system user as a member to any organization that needs prebuilds.
rbac.ResourceOrganizationMember.Type: {
policy.ActionCreate,
Expand Down Expand Up @@ -3909,7 +3912,14 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
action = policy.ActionWorkspaceStop
}

if err = q.authorizeContext(ctx, action, w); err != nil {
if action == policy.ActionDelete && w.IsPrebuild() {
if err := q.authorizeContext(ctx, action, w.PrebuildRBAC()); err != nil {
// Fallback to normal workspace auth check
if err = q.authorizeContext(ctx, action, w); err != nil {
return xerrors.Errorf("authorize context: %w", err)
}
}
} else if err = q.authorizeContext(ctx, action, w); err != nil {
return xerrors.Errorf("authorize context: %w", err)
}

Expand Down Expand Up @@ -3949,7 +3959,15 @@ func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg databa
return err
}

err = q.authorizeContext(ctx, policy.ActionUpdate, workspace)
if workspace.IsPrebuild() {
err = q.authorizeContext(ctx, policy.ActionUpdate, workspace.PrebuildRBAC())
// Fallback to normal workspace auth check
if err != nil {
err = q.authorizeContext(ctx, policy.ActionUpdate, workspace)
}
} else {
err = q.authorizeContext(ctx, policy.ActionUpdate, workspace)
}
if err != nil {
return err
}
Expand Down
17 changes: 17 additions & 0 deletions coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,26 @@ func (w Workspace) WorkspaceTable() WorkspaceTable {
}

func (w Workspace) RBACObject() rbac.Object {
// if w.IsPrebuild() {
// return w.PrebuildRBAC()
//}
return w.WorkspaceTable().RBACObject()
}

func (w Workspace) IsPrebuild() bool {
// TODO: avoid import cycle
return w.OwnerID == uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
}

func (w Workspace) PrebuildRBAC() rbac.Object {
if w.IsPrebuild() {
return rbac.ResourcePrebuiltWorkspace.WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
return w.RBACObject()
}

func (w WorkspaceTable) RBACObject() rbac.Object {
if w.DormantAt.Valid {
return w.DormantRBAC()
Expand Down
10 changes: 10 additions & 0 deletions coderd/rbac/object_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions coderd/rbac/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ var RBACPermissions = map[string]PermissionDefinition{
"workspace_dormant": {
Actions: workspaceActions,
},
"prebuilt_workspace": {
Actions: map[Action]ActionDefinition{
ActionRead: actDef("read prebuilt workspace"),
ActionUpdate: actDef("update prebuilt workspace"),
ActionDelete: actDef("delete prebuilt workspace"),
},
},
"workspace_proxy": {
Actions: map[Action]ActionDefinition{
ActionCreate: actDef("create a workspace proxy"),
Expand Down
14 changes: 8 additions & 6 deletions coderd/rbac/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceAssignOrgRole.Type: {policy.ActionRead},
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
// CRUD all files, even those they did not upload.
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
ResourceWorkspace.Type: {policy.ActionRead},
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
ResourceWorkspace.Type: {policy.ActionRead},
ResourcePrebuiltWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
// CRUD to provisioner daemons for now.
ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
// Needs to read all organizations since
Expand Down Expand Up @@ -413,7 +414,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
}),
Org: map[string][]Permission{
// Org admins should not have workspace exec perms.
organizationID.String(): append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourceAssignRole), Permissions(map[string][]policy.Action{
organizationID.String(): append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole), Permissions(map[string][]policy.Action{
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
})...),
Expand Down Expand Up @@ -493,9 +494,10 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Site: []Permission{},
Org: map[string][]Permission{
organizationID.String(): Permissions(map[string][]policy.Action{
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
ResourceWorkspace.Type: {policy.ActionRead},
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
ResourceWorkspace.Type: {policy.ActionRead},
ResourcePrebuiltWorkspace.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
// Assigning template perms requires this permission.
ResourceOrganization.Type: {policy.ActionRead},
ResourceOrganizationMember.Type: {policy.ActionRead},
Expand Down
9 changes: 9 additions & 0 deletions coderd/rbac/roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,15 @@ func TestRolePermissions(t *testing.T) {
false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor},
},
},
{
Name: "PrebuiltWorkspace",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourcePrebuiltWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgMemberMe, templateAdmin, orgTemplateAdmin},
false: {setOtherOrg, userAdmin, memberMe, orgAdmin, orgUserAdmin, orgAuditor},
},
},
// Some admin style resources
{
Name: "Licenses",
Expand Down
10 changes: 10 additions & 0 deletions coderd/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,16 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx,
tx,
func(action policy.Action, object rbac.Objecter) bool {
if object.RBACObject().Type == rbac.ResourceWorkspace.Type && action == policy.ActionDelete {
workspaceObj, ok := object.(database.Workspace)
if ok {
prebuild := workspaceObj.PrebuildRBAC()
// Fallback to normal workspace auth check
if auth := api.Authorize(r, action, prebuild); auth {
return auth
}
}
}
return api.Authorize(r, action, object)
},
audit.WorkspaceBuildBaggageFromRequest(r),
Expand Down
2 changes: 2 additions & 0 deletions codersdk/rbacresources_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/reference/api/members.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/reference/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 93 additions & 0 deletions enterprise/coderd/prebuilds/reconcile_test.go
F987
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import (
"testing"
"time"

"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/provisionersdk"

"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"golang.org/x/xerrors"
Expand Down Expand Up @@ -420,6 +426,93 @@ func TestPrebuildReconciliation(t *testing.T) {
}
}

func TestTemplateAdminDelete(t *testing.T) {
t.Parallel()

if !dbtestutil.WillUsePostgres() {
t.Skip("This test requires postgres")
}

t.Run("template admin delete prebuilds", func(t *testing.T) {
t.Parallel()

clock := quartz.NewMock(t)

// Setup.
ctx := testutil.Context(t, testutil.WaitSuperLong)
db, pubsub := dbtestutil.NewDB(t)

spy := newStoreSpy(db, nil)

logger := testutil.Logger(t)
client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{
Database: spy,
Pubsub: pubsub,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureExternalProvisionerDaemons: 1,
},
},

EntitlementsUpdateInterval: time.Second,
})

orgID := owner.OrganizationID

provisionerCloser := coderdenttest.NewExternalProvisionerDaemon(t, client, orgID, map[string]string{
provisionersdk.TagScope: provisionersdk.ScopeOrganization,
})
defer provisionerCloser.Close()

reconciler := prebuilds.NewStoreReconciler(spy, pubsub, codersdk.PrebuildsConfig{}, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer())
var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(spy)
api.AGPL.PrebuildsClaimer.Store(&claimer)

version := coderdtest.CreateTemplateVersion(t, client, orgID, templateWithAgentAndPresetsWithPrebuilds(2))
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
presets, err := client.TemplateVersionPresets(ctx, version.ID)
require.NoError(t, err)
require.Len(t, presets, 2)
preset := setupTestDBPreset(t, db, version.ID, 2, "b0rked")

templateAdminClient, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.RoleTemplateAdmin())

workspace, _ := setupTestDBPrebuild(
t,
clock,
db,
pubsub,
database.WorkspaceTransitionStart,
database.ProvisionerJobStatusSucceeded,
orgID,
preset,
template.ID,
version.ID,
)

require.NoError(t, reconciler.ReconcileAll(ctx))

runningWorkspaces, err := db.GetRunningPrebuiltWorkspaces(ctx)
require.NoError(t, err)

prebuiltWorkspace, err := db.GetWorkspaceByID(ctx, runningWorkspaces[0].ID)
require.NoError(t, err)

build, err := templateAdminClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
})
require.NoError(t, err, "delete the workspace")
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)

workspaceNew, err := client.DeletedWorkspace(ctx, prebuiltWorkspace.ID)
require.NoError(t, err)
require.Equal(t, prebuiltWorkspace.ID, workspaceNew.ID)
})
}

// brokenPublisher is used to validate that Publish() calls which always fail do not affect the reconciler's behavior,
// since the messages published are not essential but merely advisory.
type brokenPublisher struct {
Expand Down
5 changes: 5 additions & 0 deletions site/src/api/rbacresourcesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ export const RBACResourceActions: Partial<
read: "read member",
update: "update an organization member",
},
prebuilt_workspace: {
delete: "delete prebuilt workspace",
read: "read prebuilt workspace",
update: "update prebuilt workspace",
},
provisioner_daemon: {
create: "create a provisioner daemon/key",
delete: "delete a provisioner daemon/key",
Expand Down
Loading
Loading
0