Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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.

39 changes: 35 additions & 4 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,30 @@ func (q *querier) authorizeContext(ctx context.Context, action policy.Action, ob
return nil
}

// authorizeWorkspace 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) authorizeWorkspace(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))
}
Comment on lines +163 to +172
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the normal workspace check is more likely to succeed then the prebuild. So it might be a slight optimization to flip these checks.

But that will probably break some of the unit tests? No need to change this, just a thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I had considered that optimization as well. I believe it might require some test updates, so I'd prefer to merge this PR and address the optimization in a follow-up PR.

return nil
}

type authContextKey struct{}

// ActorFromContext returns the authorization subject from the context.
Expand Down Expand Up @@ -412,6 +436,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.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,8 +3939,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.authorizeWorkspace(ctx, action, w); err != nil {
return err
}

// If we're starting a workspace we need to check the template.
Expand Down Expand Up @@ -3949,8 +3980,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.authorizeWorkspace(ctx, policy.ActionUpdate, workspace); err != nil {
return err
}

Expand Down
22 changes: 22 additions & 0 deletions coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,31 @@ func (w Workspace) WorkspaceTable() WorkspaceTable {
}

func (w Workspace) RBACObject() rbac.Object {
// if w.IsPrebuild() {
// return w.AsPrebuild()
//}
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 {
// TODO: avoid import cycle
return w.OwnerID == uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0")
}

// 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()
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.

13 changes: 13 additions & 0 deletions coderd/rbac/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ var RBACPermissions = map[string]PermissionDefinition{
"workspace_dormant": {
Actions: workspaceActions,
},
"prebuilt_workspace": {
// Prebuilt_workspace actions currently apply only to delete operations.
// To successfully delete a workspace, a user must have the following permissions:
// * 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
Actions: map[Action]ActionDefinition{
ActionRead: actDef("read prebuilt workspace data"),
ActionUpdate: actDef("update prebuilt workspace settings"),
ActionDelete: actDef("delete prebuilt workspace"),
},
},
"workspace_proxy": {
Actions: map[Action]ActionDefinition{
ActionCreate: actDef("create a workspace proxy"),
Expand Down
26 changes: 18 additions & 8 deletions coderd/rbac/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.ActionRead, policy.ActionUpdate, policy.ActionDelete},
})...),
Org: map[string][]Permission{},
User: []Permission{},
Expand All @@ -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},
Expand Down Expand Up @@ -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.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,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.ActionRead, policy.ActionUpdate, policy.ActionDelete},
})...),
},
User: []Permission{},
Expand Down Expand Up @@ -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.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, orgAdmin, templateAdmin, orgTemplateAdmin},
false: {setOtherOrg, userAdmin, memberMe, orgUserAdmin, orgAuditor, orgMemberMe},
},
},
// 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 {
// 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),
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.

Loading
Loading