mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
Generated
+2
@@ -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",
|
||||
|
||||
Generated
+2
@@ -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",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package database
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
var PrebuildsSystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package prebuilds
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
var SystemUserID = uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user