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

## Description

This PR adds support for deleting prebuilt workspaces via the
authorization layer. It introduces special-case handling to ensure that
`prebuilt_workspace` permissions are evaluated when attempting to delete
a prebuilt workspace, falling back to the standard `workspace` resource
as needed.

Prebuilt workspaces are a subset of workspaces, identified by having
`owner_id` set to `PREBUILD_SYSTEM_USER`.
This means:
* A user with `prebuilt_workspace.delete` permission is allowed to
**delete only prebuilt workspaces**.
* A user with `workspace.delete` permission can **delete both normal and
prebuilt workspaces**.

⚠️ This implementation is scoped to **deletion operations only**. No
other operations are currently supported for the `prebuilt_workspace`
resource.

To delete a workspace, users must have the following permissions:
* `workspace.read`: to read the current workspace state
* `update`: to modify workspace metadata and related resources during
deletion (e.g., updating the `deleted` field in the database)
* `delete`: to perform the actual deletion of the workspace

## Changes

* Introduced `authorizeWorkspace()` helper to handle prebuilt workspace
authorization logic.
* Ensured both `prebuilt_workspace` and `workspace` permissions are
checked.
* Added comments to clarify the current behavior and limitations.
* Moved `SystemUserID` constant from the `prebuilds` package to the
`database` package `PrebuildsSystemUserID` to resolve an import cycle
(commit
https://github.com/coder/coder/pull/18333/commits/f24e4ab4b6f0a56726fd04be2d7302c9fdb52d53).
* Update middleware `ExtractOrganizationMember` to include system user
members.
This commit is contained in:
Susana Ferreira
2025-06-20 17:36:32 +01:00
committed by GitHub
parent d61353f468
commit 72f7d70bab
29 changed files with 493 additions and 63 deletions
+230
View File
@@ -2,9 +2,18 @@ package cli_test
import (
"context"
"database/sql"
"fmt"
"io"
"testing"
"time"
"github.com/google/uuid"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/quartz"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -209,4 +218,225 @@ func TestDelete(t *testing.T) {
cancel()
<-doneChan
})
t.Run("Prebuilt workspace delete permissions", func(t *testing.T) {
t.Parallel()
if !dbtestutil.WillUsePostgres() {
t.Skip("this test requires postgres")
}
clock := quartz.NewMock(t)
ctx := testutil.Context(t, testutil.WaitSuperLong)
// Setup
db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
client, _ := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
Database: db,
Pubsub: pb,
IncludeProvisionerDaemon: true,
})
owner := coderdtest.CreateFirstUser(t, client)
orgID := owner.OrganizationID
// Given a template version with a preset and a template
version := coderdtest.CreateTemplateVersion(t, client, orgID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
preset := setupTestDBPreset(t, db, version.ID)
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
cases := []struct {
name string
client *codersdk.Client
expectedPrebuiltDeleteErrMsg string
expectedWorkspaceDeleteErrMsg string
}{
// Users with the OrgAdmin role should be able to delete both normal and prebuilt workspaces
{
name: "OrgAdmin",
client: func() *codersdk.Client {
client, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.ScopedRoleOrgAdmin(orgID))
return client
}(),
},
// Users with the TemplateAdmin role should be able to delete prebuilt workspaces, but not normal workspaces
{
name: "TemplateAdmin",
client: func() *codersdk.Client {
client, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.RoleTemplateAdmin())
return client
}(),
expectedWorkspaceDeleteErrMsg: "unexpected status code 403: You do not have permission to delete this workspace.",
},
// Users with the OrgTemplateAdmin role should be able to delete prebuilt workspaces, but not normal workspaces
{
name: "OrgTemplateAdmin",
client: func() *codersdk.Client {
client, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.ScopedRoleOrgTemplateAdmin(orgID))
return client
}(),
expectedWorkspaceDeleteErrMsg: "unexpected status code 403: You do not have permission to delete this workspace.",
},
// Users with the Member role should not be able to delete prebuilt or normal workspaces
{
name: "Member",
client: func() *codersdk.Client {
client, _ := coderdtest.CreateAnotherUser(t, client, orgID, rbac.RoleMember())
return client
}(),
expectedPrebuiltDeleteErrMsg: "unexpected status code 404: Resource not found or you do not have access to this resource",
expectedWorkspaceDeleteErrMsg: "unexpected status code 404: Resource not found or you do not have access to this resource",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Create one prebuilt workspace (owned by system user) and one normal workspace (owned by a user)
// Each workspace is persisted in the DB along with associated workspace jobs and builds.
dbPrebuiltWorkspace := setupTestDBWorkspace(t, clock, db, pb, orgID, database.PrebuildsSystemUserID, template.ID, version.ID, preset.ID)
userWorkspaceOwner, err := client.User(context.Background(), "testUser")
require.NoError(t, err)
dbUserWorkspace := setupTestDBWorkspace(t, clock, db, pb, orgID, userWorkspaceOwner.ID, template.ID, version.ID, preset.ID)
assertWorkspaceDelete := func(
runClient *codersdk.Client,
workspace database.Workspace,
workspaceOwner string,
expectedErr string,
) {
t.Helper()
// Attempt to delete the workspace as the test client
inv, root := clitest.New(t, "delete", workspaceOwner+"/"+workspace.Name, "-y")
clitest.SetupConfig(t, runClient, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
var runErr error
go func() {
defer close(doneChan)
runErr = inv.Run()
}()
// Validate the result based on the expected error message
if expectedErr != "" {
<-doneChan
require.Error(t, runErr)
require.Contains(t, runErr.Error(), expectedErr)
} else {
pty.ExpectMatch("has been deleted")
<-doneChan
// When running with the race detector on, we sometimes get an EOF.
if runErr != nil {
assert.ErrorIs(t, runErr, io.EOF)
}
// Verify that the workspace is now marked as deleted
_, err := client.Workspace(context.Background(), workspace.ID)
require.ErrorContains(t, err, "was deleted")
}
}
// Ensure at least one prebuilt workspace is reported as running in the database
testutil.Eventually(ctx, t, func(ctx context.Context) (done bool) {
running, err := db.GetRunningPrebuiltWorkspaces(ctx)
if !assert.NoError(t, err) || !assert.GreaterOrEqual(t, len(running), 1) {
return false
}
return true
}, testutil.IntervalMedium, "running prebuilt workspaces timeout")
runningWorkspaces, err := db.GetRunningPrebuiltWorkspaces(ctx)
require.NoError(t, err)
require.GreaterOrEqual(t, len(runningWorkspaces), 1)
// Get the full prebuilt workspace object from the DB
prebuiltWorkspace, err := db.GetWorkspaceByID(ctx, dbPrebuiltWorkspace.ID)
require.NoError(t, err)
// Assert the prebuilt workspace deletion
assertWorkspaceDelete(tc.client, prebuiltWorkspace, "prebuilds", tc.expectedPrebuiltDeleteErrMsg)
// Get the full user workspace object from the DB
userWorkspace, err := db.GetWorkspaceByID(ctx, dbUserWorkspace.ID)
require.NoError(t, err)
// Assert the user workspace deletion
assertWorkspaceDelete(tc.client, userWorkspace, userWorkspaceOwner.Username, tc.expectedWorkspaceDeleteErrMsg)
})
}
})
}
func setupTestDBPreset(
t *testing.T,
db database.Store,
templateVersionID uuid.UUID,
) database.TemplateVersionPreset {
t.Helper()
preset := dbgen.Preset(t, db, database.InsertPresetParams{
TemplateVersionID: templateVersionID,
Name: "preset-test",
DesiredInstances: sql.NullInt32{
Valid: true,
Int32: 1,
},
})
dbgen.PresetParameter(t, db, database.InsertPresetParametersParams{
TemplateVersionPresetID: preset.ID,
Names: []string{"test"},
Values: []string{"test"},
})
return preset
}
func setupTestDBWorkspace(
t *testing.T,
clock quartz.Clock,
db database.Store,
ps pubsub.Pubsub,
orgID uuid.UUID,
ownerID uuid.UUID,
templateID uuid.UUID,
templateVersionID uuid.UUID,
presetID uuid.UUID,
) database.WorkspaceTable {
t.Helper()
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
TemplateID: templateID,
OrganizationID: orgID,
OwnerID: ownerID,
Deleted: false,
CreatedAt: time.Now().Add(-time.Hour * 2),
})
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
InitiatorID: ownerID,
CreatedAt: time.Now().Add(-time.Hour * 2),
StartedAt: sql.NullTime{Time: clock.Now().Add(-time.Hour * 2), Valid: true},
CompletedAt: sql.NullTime{Time: clock.Now().Add(-time.Hour), Valid: true},
OrganizationID: orgID,
})
workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
InitiatorID: ownerID,
TemplateVersionID: templateVersionID,
JobID: job.ID,
TemplateVersionPresetID: uuid.NullUUID{UUID: presetID, Valid: true},
Transition: database.WorkspaceTransitionStart,
CreatedAt: clock.Now(),
})
dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{
{
WorkspaceBuildID: workspaceBuild.ID,
Name: "test",
Value: "test",
},
})
return workspace
}
+2
View File
@@ -15259,6 +15259,7 @@ const docTemplate = `{
"oauth2_app_secret",
"organization",
"organization_member",
"prebuilt_workspace",
"provisioner_daemon",
"provisioner_jobs",
"replicas",
@@ -15298,6 +15299,7 @@ const docTemplate = `{
"ResourceOauth2AppSecret",
"ResourceOrganization",
"ResourceOrganizationMember",
"ResourcePrebuiltWorkspace",
"ResourceProvisionerDaemon",
"ResourceProvisionerJobs",
"ResourceReplicas",
+2
View File
@@ -13851,6 +13851,7 @@
"oauth2_app_secret",
"organization",
"organization_member",
"prebuilt_workspace",
"provisioner_daemon",
"provisioner_jobs",
"replicas",
@@ -13890,6 +13891,7 @@
"ResourceOauth2AppSecret",
"ResourceOrganization",
"ResourceOrganizationMember",
"ResourcePrebuiltWorkspace",
"ResourceProvisionerDaemon",
"ResourceProvisionerJobs",
"ResourceReplicas",
+5
View File
@@ -0,0 +1,5 @@
package database
import "github.com/google/uuid"
var PrebuildsSystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
+36 -6
View File
@@ -21,7 +21,6 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints"
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
"github.com/coder/coder/v2/coderd/prebuilds"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/rbac/rolestore"
@@ -150,6 +149,30 @@ func (q *querier) authorizeContext(ctx context.Context, action policy.Action, ob
return nil
}
// authorizePrebuiltWorkspace handles authorization for workspace resource types.
// prebuilt_workspaces are a subset of workspaces, currently limited to
// supporting delete operations. Therefore, if the action is delete or
// update and the workspace is a prebuild, a prebuilt-specific authorization
// is attempted first. If that fails, it falls back to normal workspace
// authorization.
// Note: Delete operations of workspaces requires both update and delete
// permissions.
func (q *querier) authorizePrebuiltWorkspace(ctx context.Context, action policy.Action, workspace database.Workspace) error {
var prebuiltErr error
// Special handling for prebuilt_workspace deletion authorization check
if (action == policy.ActionUpdate || action == policy.ActionDelete) && workspace.IsPrebuild() {
// Try prebuilt-specific authorization first
if prebuiltErr = q.authorizeContext(ctx, action, workspace.AsPrebuild()); prebuiltErr == nil {
return nil
}
}
// Fallback to normal workspace authorization check
if err := q.authorizeContext(ctx, action, workspace); err != nil {
return xerrors.Errorf("authorize context: %w", errors.Join(prebuiltErr, err))
}
return nil
}
type authContextKey struct{}
// ActorFromContext returns the authorization subject from the context.
@@ -399,7 +422,7 @@ var (
subjectPrebuildsOrchestrator = rbac.Subject{
Type: rbac.SubjectTypePrebuildsOrchestrator,
FriendlyName: "Prebuilds Orchestrator",
ID: prebuilds.SystemUserID.String(),
ID: database.PrebuildsSystemUserID.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "prebuilds-orchestrator"},
@@ -412,6 +435,12 @@ var (
policy.ActionCreate, policy.ActionDelete, policy.ActionRead, policy.ActionUpdate,
policy.ActionWorkspaceStart, policy.ActionWorkspaceStop,
},
// PrebuiltWorkspaces are a subset of Workspaces.
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
rbac.ResourcePrebuiltWorkspace.Type: {
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,
@@ -3953,8 +3982,9 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
action = policy.ActionWorkspaceStop
}
if err = q.authorizeContext(ctx, action, w); err != nil {
return xerrors.Errorf("authorize context: %w", err)
// Special handling for prebuilt workspace deletion
if err := q.authorizePrebuiltWorkspace(ctx, action, w); err != nil {
return err
}
// If we're starting a workspace we need to check the template.
@@ -3993,8 +4023,8 @@ func (q *querier) InsertWorkspaceBuildParameters(ctx context.Context, arg databa
return err
}
err = q.authorizeContext(ctx, policy.ActionUpdate, workspace)
if err != nil {
// Special handling for prebuilt workspace deletion
if err := q.authorizePrebuiltWorkspace(ctx, policy.ActionUpdate, workspace); err != nil {
return err
}
+60
View File
@@ -5562,3 +5562,63 @@ func (s *MethodTestSuite) TestChat() {
}).Asserts(c, policy.ActionUpdate)
}))
}
func (s *MethodTestSuite) TestAuthorizePrebuiltWorkspace() {
s.Run("PrebuildDelete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
o := dbgen.Organization(s.T(), db, database.Organization{})
tpl := dbgen.Template(s.T(), db, database.Template{
OrganizationID: o.ID,
CreatedBy: u.ID,
})
w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{
TemplateID: tpl.ID,
OrganizationID: o.ID,
OwnerID: database.PrebuildsSystemUserID,
})
pj := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
OrganizationID: o.ID,
})
tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
OrganizationID: o.ID,
CreatedBy: u.ID,
})
check.Args(database.InsertWorkspaceBuildParams{
WorkspaceID: w.ID,
Transition: database.WorkspaceTransitionDelete,
Reason: database.BuildReasonInitiator,
TemplateVersionID: tv.ID,
JobID: pj.ID,
}).Asserts(w.AsPrebuild(), policy.ActionDelete)
}))
s.Run("PrebuildUpdate/InsertWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
o := dbgen.Organization(s.T(), db, database.Organization{})
tpl := dbgen.Template(s.T(), db, database.Template{
OrganizationID: o.ID,
CreatedBy: u.ID,
})
w := dbgen.Workspace(s.T(), db, database.WorkspaceTable{
TemplateID: tpl.ID,
OrganizationID: o.ID,
OwnerID: database.PrebuildsSystemUserID,
})
pj := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
OrganizationID: o.ID,
})
tv := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: tpl.ID, Valid: true},
OrganizationID: o.ID,
CreatedBy: u.ID,
})
wb := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{
JobID: pj.ID,
WorkspaceID: w.ID,
TemplateVersionID: tv.ID,
})
check.Args(database.InsertWorkspaceBuildParametersParams{
WorkspaceBuildID: wb.ID,
}).Asserts(w.AsPrebuild(), policy.ActionUpdate)
}))
}
+2 -4
View File
@@ -23,11 +23,9 @@ import (
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/notifications/types"
"github.com/coder/coder/v2/coderd/prebuilds"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/notifications/types"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/regosql"
"github.com/coder/coder/v2/coderd/util/slice"
@@ -160,7 +158,7 @@ func New() database.Store {
q.mutex.Lock()
// We can't insert this user using the interface, because it's a system user.
q.data.users = append(q.data.users, database.User{
ID: prebuilds.SystemUserID,
ID: database.PrebuildsSystemUserID,
Email: "prebuilds@coder.com",
Username: "prebuilds",
CreatedAt: dbtime.Now(),
+36
View File
@@ -229,6 +229,24 @@ func (w Workspace) RBACObject() rbac.Object {
return w.WorkspaceTable().RBACObject()
}
// IsPrebuild returns true if the workspace is a prebuild workspace.
// A workspace is considered a prebuild if its owner is the prebuild system user.
func (w Workspace) IsPrebuild() bool {
return w.OwnerID == PrebuildsSystemUserID
}
// AsPrebuild returns the RBAC object corresponding to the workspace type.
// If the workspace is a prebuild, it returns a prebuilt_workspace RBAC object.
// Otherwise, it returns a normal workspace RBAC object.
func (w Workspace) AsPrebuild() 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()
@@ -246,6 +264,24 @@ func (w WorkspaceTable) DormantRBAC() rbac.Object {
WithOwner(w.OwnerID.String())
}
// IsPrebuild returns true if the workspace is a prebuild workspace.
// A workspace is considered a prebuild if its owner is the prebuild system user.
func (w WorkspaceTable) IsPrebuild() bool {
return w.OwnerID == PrebuildsSystemUserID
}
// AsPrebuild returns the RBAC object corresponding to the workspace type.
// If the workspace is a prebuild, it returns a prebuilt_workspace RBAC object.
// Otherwise, it returns a normal workspace RBAC object.
func (w WorkspaceTable) AsPrebuild() rbac.Object {
if w.IsPrebuild() {
return rbac.ResourcePrebuiltWorkspace.WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
return w.RBACObject()
}
func (m OrganizationMember) RBACObject() rbac.Object {
return rbac.ResourceOrganizationMember.
WithID(m.UserID).
+1 -2
View File
@@ -27,7 +27,6 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/migrations"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/prebuilds"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
@@ -1418,7 +1417,7 @@ func TestGetUsers_IncludeSystem(t *testing.T) {
for _, u := range users {
if u.IsSystem {
foundSystemUser = true
require.Equal(t, prebuilds.SystemUserID, u.ID)
require.Equal(t, database.PrebuildsSystemUserID, u.ID)
} else {
foundRegularUser = true
require.Equalf(t, other.ID.String(), u.ID.String(), "found unexpected regular user")
+1 -1
View File
@@ -180,7 +180,7 @@ func ExtractOrganizationMember(ctx context.Context, auth func(r *http.Request, a
organizationMembers, err := db.OrganizationMembers(ctx, database.OrganizationMembersParams{
OrganizationID: orgID,
UserID: user.ID,
IncludeSystem: false,
IncludeSystem: true,
})
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
-5
View File
@@ -1,5 +0,0 @@
package prebuilds
import "github.com/google/uuid"
var SystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
+9
View File
@@ -222,6 +222,14 @@ var (
Type: "organization_member",
}
// ResourcePrebuiltWorkspace
// Valid Actions
// - "ActionDelete" :: delete prebuilt workspace
// - "ActionUpdate" :: update prebuilt workspace settings
ResourcePrebuiltWorkspace = Object{
Type: "prebuilt_workspace",
}
// ResourceProvisionerDaemon
// Valid Actions
// - "ActionCreate" :: create a provisioner daemon/key
@@ -389,6 +397,7 @@ func AllResources() []Objecter {
ResourceOauth2AppSecret,
ResourceOrganization,
ResourceOrganizationMember,
ResourcePrebuiltWorkspace,
ResourceProvisionerDaemon,
ResourceProvisionerJobs,
ResourceReplicas,
+14
View File
@@ -102,6 +102,20 @@ var RBACPermissions = map[string]PermissionDefinition{
"workspace_dormant": {
Actions: workspaceActions,
},
"prebuilt_workspace": {
// Prebuilt_workspace actions currently apply only to delete operations.
// To successfully delete a prebuilt workspace, a user must have the following permissions:
// * workspace.read: to read the current workspace state
// * update: to modify workspace metadata and related resources during deletion
// (e.g., updating the deleted field in the database)
// * delete: to perform the actual deletion of the workspace
// If the user lacks prebuilt_workspace update or delete permissions,
// the authorization will always fall back to the corresponding permissions on workspace.
Actions: map[Action]ActionDefinition{
ActionUpdate: actDef("update prebuilt workspace settings"),
ActionDelete: actDef("delete prebuilt workspace"),
},
},
"workspace_proxy": {
Actions: map[Action]ActionDefinition{
ActionCreate: actDef("create a workspace proxy"),
+18 -8
View File
@@ -270,11 +270,15 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Site: append(
// Workspace dormancy and workspace are omitted.
// Workspace is specifically handled based on the opts.NoOwnerWorkspaceExec
allPermsExcept(ResourceWorkspaceDormant, ResourceWorkspace),
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace),
// This adds back in the Workspace permissions.
Permissions(map[string][]policy.Action{
ResourceWorkspace.Type: ownerWorkspaceActions,
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
// PrebuiltWorkspaces are a subset of Workspaces.
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...),
Org: map[string][]Permission{},
User: []Permission{},
@@ -290,7 +294,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceWorkspaceProxy.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember),
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceUser, ResourceOrganizationMember),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build, ssh, or exec
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
@@ -335,8 +339,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.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
@@ -413,9 +418,13 @@ 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),
// PrebuiltWorkspaces are a subset of Workspaces.
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...),
},
User: []Permission{},
@@ -493,9 +502,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.ActionUpdate, policy.ActionDelete},
// Assigning template perms requires this permission.
ResourceOrganization.Type: {policy.ActionRead},
ResourceOrganizationMember.Type: {policy.ActionRead},
+11
View File
@@ -5,6 +5,8 @@ import (
"fmt"
"testing"
"github.com/coder/coder/v2/coderd/database"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
@@ -496,6 +498,15 @@ func TestRolePermissions(t *testing.T) {
false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor},
},
},
{
Name: "PrebuiltWorkspace",
Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourcePrebuiltWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(database.PrebuildsSystemUserID.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
false: {setOtherOrg, userAdmin, memberMe, orgUserAdmin, orgAuditor, orgMemberMe},
},
},
// Some admin style resources
{
Name: "Licenses",
+10
View File
@@ -392,6 +392,16 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx,
tx,
func(action policy.Action, object rbac.Objecter) bool {
// Special handling for prebuilt workspace deletion
if object.RBACObject().Type == rbac.ResourceWorkspace.Type && action == policy.ActionDelete {
if workspaceObj, ok := object.(database.Workspace); ok {
// Try prebuilt-specific authorization first
if auth := api.Authorize(r, action, workspaceObj.AsPrebuild()); auth {
return auth
}
}
}
// Fallback to default authorization
return api.Authorize(r, action, object)
},
audit.WorkspaceBuildBaggageFromRequest(r),
+12 -1
View File
@@ -918,7 +918,18 @@ func (b *Builder) authorize(authFunc func(action policy.Action, object rbac.Obje
msg := fmt.Sprintf("Transition %q not supported.", b.trans)
return BuildError{http.StatusBadRequest, msg, xerrors.New(msg)}
}
if !authFunc(action, b.workspace) {
// Special handling for prebuilt workspace deletion
authorized := false
if action == policy.ActionDelete && b.workspace.IsPrebuild() && authFunc(action, b.workspace.AsPrebuild()) {
authorized = true
}
// Fallback to default authorization
if !authorized && authFunc(action, b.workspace) {
authorized = true
}
if !authorized {
if authFunc(policy.ActionRead, b.workspace) {
// If the user can read the workspace, but not delete/create/update. Show
// a more helpful error. They are allowed to know the workspace exists.
+2
View File
@@ -28,6 +28,7 @@ const (
ResourceOauth2AppSecret RBACResource = "oauth2_app_secret"
ResourceOrganization RBACResource = "organization"
ResourceOrganizationMember RBACResource = "organization_member"
ResourcePrebuiltWorkspace RBACResource = "prebuilt_workspace"
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
ResourceProvisionerJobs RBACResource = "provisioner_jobs"
ResourceReplicas RBACResource = "replicas"
@@ -91,6 +92,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
ResourceOauth2AppSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourcePrebuiltWorkspace: {ActionDelete, ActionUpdate},
ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceProvisionerJobs: {ActionCreate, ActionRead, ActionUpdate},
ResourceReplicas: {ActionRead},
+5
View File
@@ -206,6 +206,7 @@ Status Code **200**
| `resource_type` | `oauth2_app_secret` |
| `resource_type` | `organization` |
| `resource_type` | `organization_member` |
| `resource_type` | `prebuilt_workspace` |
| `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_jobs` |
| `resource_type` | `replicas` |
@@ -375,6 +376,7 @@ Status Code **200**
| `resource_type` | `oauth2_app_secret` |
| `resource_type` | `organization` |
| `resource_type` | `organization_member` |
| `resource_type` | `prebuilt_workspace` |
| `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_jobs` |
| `resource_type` | `replicas` |
@@ -544,6 +546,7 @@ Status Code **200**
| `resource_type` | `oauth2_app_secret` |
| `resource_type` | `organization` |
| `resource_type` | `organization_member` |
| `resource_type` | `prebuilt_workspace` |
| `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_jobs` |
| `resource_type` | `replicas` |
@@ -682,6 +685,7 @@ Status Code **200**
| `resource_type` | `oauth2_app_secret` |
| `resource_type` | `organization` |
| `resource_type` | `organization_member` |
| `resource_type` | `prebuilt_workspace` |
| `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_jobs` |
| `resource_type` | `replicas` |
@@ -1042,6 +1046,7 @@ Status Code **200**
| `resource_type` | `oauth2_app_secret` |
| `resource_type` | `organization` |
| `resource_type` | `organization_member` |
| `resource_type` | `prebuilt_workspace` |
| `resource_type` | `provisioner_daemon` |
| `resource_type` | `provisioner_jobs` |
| `resource_type` | `replicas` |
+1
View File
@@ -6329,6 +6329,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
| `oauth2_app_secret` |
| `organization` |
| `organization_member` |
| `prebuilt_workspace` |
| `provisioner_daemon` |
| `provisioner_jobs` |
| `replicas` |
+1 -3
View File
@@ -6,8 +6,6 @@ import (
"testing"
"time"
"github.com/coder/coder/v2/coderd/prebuilds"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
@@ -833,7 +831,7 @@ func TestGroup(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// nolint:gocritic // "This client is operating as the owner user" is fine in this case.
prebuildsUser, err := client.User(ctx, prebuilds.SystemUserID.String())
prebuildsUser, err := client.User(ctx, database.PrebuildsSystemUserID.String())
require.NoError(t, err)
// The 'Everyone' group always has an ID that matches the organization ID.
group, err := userAdminClient.Group(ctx, user.OrganizationID)
+1 -1
View File
@@ -47,7 +47,7 @@ func (c EnterpriseClaimer) Claim(
}
func (EnterpriseClaimer) Initiator() uuid.UUID {
return prebuilds.SystemUserID
return database.PrebuildsSystemUserID
}
var _ prebuilds.Claimer = &EnterpriseClaimer{}
@@ -12,7 +12,6 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds"
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
)
@@ -74,14 +73,14 @@ func TestReconcileAll(t *testing.T) {
// dbmem doesn't ensure membership to the default organization
dbgen.OrganizationMember(t, db, database.OrganizationMember{
OrganizationID: defaultOrg.ID,
UserID: agplprebuilds.SystemUserID,
UserID: database.PrebuildsSystemUserID,
})
}
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: unrelatedOrg.ID, UserID: agplprebuilds.SystemUserID})
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: unrelatedOrg.ID, UserID: database.PrebuildsSystemUserID})
if tc.preExistingMembership {
// System user already a member of both orgs.
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: targetOrg.ID, UserID: agplprebuilds.SystemUserID})
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: targetOrg.ID, UserID: database.PrebuildsSystemUserID})
}
presets := []database.GetTemplatePresetsWithPrebuildsRow{newPresetRow(unrelatedOrg.ID)}
@@ -91,7 +90,7 @@ func TestReconcileAll(t *testing.T) {
// Verify memberships before reconciliation.
preReconcileMemberships, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{
UserID: agplprebuilds.SystemUserID,
UserID: database.PrebuildsSystemUserID,
})
require.NoError(t, err)
expectedMembershipsBefore := []uuid.UUID{defaultOrg.ID, unrelatedOrg.ID}
@@ -102,11 +101,11 @@ func TestReconcileAll(t *testing.T) {
// Reconcile
reconciler := prebuilds.NewStoreMembershipReconciler(db, clock)
require.NoError(t, reconciler.ReconcileAll(ctx, agplprebuilds.SystemUserID, presets))
require.NoError(t, reconciler.ReconcileAll(ctx, database.PrebuildsSystemUserID, presets))
// Verify memberships after reconciliation.
postReconcileMemberships, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{
UserID: agplprebuilds.SystemUserID,
UserID: database.PrebuildsSystemUserID,
})
require.NoError(t, err)
expectedMembershipsAfter := expectedMembershipsBefore
@@ -20,7 +20,6 @@ import (
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
"github.com/coder/coder/v2/testutil"
@@ -55,8 +54,8 @@ func TestMetricsCollector(t *testing.T) {
name: "prebuild provisioned but not completed",
transitions: allTransitions,
jobStatuses: allJobStatusesExcept(database.ProvisionerJobStatusPending, database.ProvisionerJobStatusRunning, database.ProvisionerJobStatusCanceling),
initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID},
ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID},
initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID},
ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID},
metrics: []metricCheck{
{prebuilds.MetricCreatedCount, ptr.To(1.0), true},
{prebuilds.MetricClaimedCount, ptr.To(0.0), true},
@@ -72,8 +71,8 @@ func TestMetricsCollector(t *testing.T) {
name: "prebuild running",
transitions: []database.WorkspaceTransition{database.WorkspaceTransitionStart},
jobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusSucceeded},
initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID},
ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID},
initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID},
ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID},
metrics: []metricCheck{
{prebuilds.MetricCreatedCount, ptr.To(1.0), true},
{prebuilds.MetricClaimedCount, ptr.To(0.0), true},
@@ -89,8 +88,8 @@ func TestMetricsCollector(t *testing.T) {
name: "prebuild failed",
transitions: allTransitions,
jobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusFailed},
initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID},
ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID, uuid.New()},
initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID},
ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID, uuid.New()},
metrics: []metricCheck{
{prebuilds.MetricCreatedCount, ptr.To(1.0), true},
{prebuilds.MetricFailedCount, ptr.To(1.0), true},
@@ -105,8 +104,8 @@ func TestMetricsCollector(t *testing.T) {
name: "prebuild eligible",
transitions: []database.WorkspaceTransition{database.WorkspaceTransitionStart},
jobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusSucceeded},
initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID},
ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID},
initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID},
ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID},
metrics: []metricCheck{
{prebuilds.MetricCreatedCount, ptr.To(1.0), true},
{prebuilds.MetricClaimedCount, ptr.To(0.0), true},
@@ -122,8 +121,8 @@ func TestMetricsCollector(t *testing.T) {
name: "prebuild ineligible",
transitions: allTransitions,
jobStatuses: allJobStatusesExcept(database.ProvisionerJobStatusSucceeded),
initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID},
ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID},
initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID},
ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID},
metrics: []metricCheck{
{prebuilds.MetricCreatedCount, ptr.To(1.0), true},
{prebuilds.MetricClaimedCount, ptr.To(0.0), true},
@@ -139,7 +138,7 @@ func TestMetricsCollector(t *testing.T) {
name: "prebuild claimed",
transitions: allTransitions,
jobStatuses: allJobStatuses,
initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID},
initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID},
ownerIDs: []uuid.UUID{uuid.New()},
metrics: []metricCheck{
{prebuilds.MetricCreatedCount, ptr.To(1.0), true},
@@ -169,8 +168,8 @@ func TestMetricsCollector(t *testing.T) {
name: "deleted templates should not be included in exported metrics",
transitions: allTransitions,
jobStatuses: allJobStatuses,
initiatorIDs: []uuid.UUID{agplprebuilds.SystemUserID},
ownerIDs: []uuid.UUID{agplprebuilds.SystemUserID, uuid.New()},
initiatorIDs: []uuid.UUID{database.PrebuildsSystemUserID},
ownerIDs: []uuid.UUID{database.PrebuildsSystemUserID, uuid.New()},
metrics: nil,
templateDeleted: []bool{true},
eligible: []bool{false},
@@ -209,7 +208,7 @@ func TestMetricsCollector(t *testing.T) {
reconciler := prebuilds.NewStoreReconciler(db, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
ctx := testutil.Context(t, testutil.WaitLong)
createdUsers := []uuid.UUID{agplprebuilds.SystemUserID}
createdUsers := []uuid.UUID{database.PrebuildsSystemUserID}
for _, user := range slices.Concat(test.ownerIDs, test.initiatorIDs) {
if !slices.Contains(createdUsers, user) {
dbgen.User(t, db, database.User{
@@ -327,8 +326,8 @@ func TestMetricsCollector_DuplicateTemplateNames(t *testing.T) {
test := testCase{
transition: database.WorkspaceTransitionStart,
jobStatus: database.ProvisionerJobStatusSucceeded,
initiatorID: agplprebuilds.SystemUserID,
ownerID: agplprebuilds.SystemUserID,
initiatorID: database.PrebuildsSystemUserID,
ownerID: database.PrebuildsSystemUserID,
metrics: []metricCheck{
{prebuilds.MetricCreatedCount, ptr.To(1.0), true},
{prebuilds.MetricClaimedCount, ptr.To(0.0), true},
+4 -4
View File
@@ -265,7 +265,7 @@ func (c *StoreReconciler) ReconcileAll(ctx context.Context) error {
}
membershipReconciler := NewStoreMembershipReconciler(c.store, c.clock)
err = membershipReconciler.ReconcileAll(ctx, prebuilds.SystemUserID, snapshot.Presets)
err = membershipReconciler.ReconcileAll(ctx, database.PrebuildsSystemUserID, snapshot.Presets)
if err != nil {
return xerrors.Errorf("reconcile prebuild membership: %w", err)
}
@@ -676,7 +676,7 @@ func (c *StoreReconciler) createPrebuiltWorkspace(ctx context.Context, prebuiltW
ID: prebuiltWorkspaceID,
CreatedAt: now,
UpdatedAt: now,
OwnerID: prebuilds.SystemUserID,
OwnerID: database.PrebuildsSystemUserID,
OrganizationID: template.OrganizationID,
TemplateID: template.ID,
Name: name,
@@ -718,7 +718,7 @@ func (c *StoreReconciler) deletePrebuiltWorkspace(ctx context.Context, prebuiltW
return xerrors.Errorf("failed to get template: %w", err)
}
if workspace.OwnerID != prebuilds.SystemUserID {
if workspace.OwnerID != database.PrebuildsSystemUserID {
return xerrors.Errorf("prebuilt workspace is not owned by prebuild user anymore, probably it was claimed")
}
@@ -761,7 +761,7 @@ func (c *StoreReconciler) provision(
builder := wsbuilder.New(workspace, transition).
Reason(database.BuildReasonInitiator).
Initiator(prebuilds.SystemUserID).
Initiator(database.PrebuildsSystemUserID).
MarkPrebuild()
if transition != database.WorkspaceTransitionDelete {
@@ -33,7 +33,6 @@ import (
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/pubsub"
agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
"github.com/coder/coder/v2/testutil"
@@ -2021,7 +2020,7 @@ func setupTestDBPrebuild(
opts ...prebuildOption,
) (database.WorkspaceTable, database.WorkspaceBuild) {
t.Helper()
return setupTestDBWorkspace(t, clock, db, ps, transition, prebuildStatus, orgID, preset, templateID, templateVersionID, agplprebuilds.SystemUserID, agplprebuilds.SystemUserID, opts...)
return setupTestDBWorkspace(t, clock, db, ps, transition, prebuildStatus, orgID, preset, templateID, templateVersionID, database.PrebuildsSystemUserID, database.PrebuildsSystemUserID, opts...)
}
func setupTestDBWorkspace(
+1 -2
View File
@@ -32,7 +32,6 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/prebuilds"
"github.com/coder/coder/v2/coderd/provisionerdserver"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
@@ -496,7 +495,7 @@ func TestCreateUserWorkspace(t *testing.T) {
}).Do()
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
OwnerID: prebuilds.SystemUserID,
OwnerID: database.PrebuildsSystemUserID,
TemplateID: tv.Template.ID,
}).Seed(database.WorkspaceBuild{
TemplateVersionID: tv.TemplateVersion.ID,
+4
View File
@@ -123,6 +123,10 @@ export const RBACResourceActions: Partial<
read: "read member",
update: "update an organization member",
},
prebuilt_workspace: {
delete: "delete prebuilt workspace",
update: "update prebuilt workspace settings",
},
provisioner_daemon: {
create: "create a provisioner daemon/key",
delete: "delete a provisioner daemon/key",
+2
View File
@@ -2192,6 +2192,7 @@ export type RBACResource =
| "oauth2_app_secret"
| "organization"
| "organization_member"
| "prebuilt_workspace"
| "provisioner_daemon"
| "provisioner_jobs"
| "replicas"
@@ -2231,6 +2232,7 @@ export const RBACResources: RBACResource[] = [
"oauth2_app_secret",
"organization",
"organization_member",
"prebuilt_workspace",
"provisioner_daemon",
"provisioner_jobs",
"replicas",