Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
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