mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
0e23625c25
Member-level perms in OrgPermissions only fire when
input.object.owner == input.subject.id (see the org_member rule in
coderd/rbac/policy.rego). Resources whose RBACObject() does not set
WithOwner(...) at production call sites can never satisfy that
condition; granting them at Member scope is dead code. PR 1's
enumeration inherited these from the legacy allPermsExcept(...)
wildcard. This commit drops them so the floor matches its documented
scope and adds an "Intentionally omitted" block in roles.go listing
each removed type and the reason it stays out, for posterity.
Removed from both OrgMemberPermissions and OrgServiceAccountPermissions
Member maps:
- ResourceTemplate {read, use}
Template.RBACObject sets InOrg and ACLs but no Owner. Org-member
template.use is granted via the "Everyone" ACL path
(acl_group_list[org_owner] populated on each template's
GroupACL); that is the rule that fires in createWorkspace, not
the Member-level grant.
- ResourceGroup {read}
Group.RBACObject sets a per-group GroupACL granting read to the
group's own ID, but no Owner. "Groups I'm a member of can read
themselves" is the ACL path. Reading other groups requires
a higher role.
- ResourceWorkspaceProxy {read}
WorkspaceProxy.RBACObject sets only WithID. All production call
sites use the bare resource; Member-level grant never fires.
- ResourceProvisionerJobs {*}
No DB model implements RBACObject. Handler call sites use
.InOrg(org.ID) only; coderd/provisionerjobs.go:100 documents
the intent as "only owners and template admins can access
provisioner jobs."
- ResourceWorkspaceAgentResourceMonitor {*}
Dbauthz call sites use the bare resource for system / telemetry
reads. Owner-scoped checks (e.g.
FetchVolumesResourceMonitorsByAgentID) route through the
workspace object instead, so the Member-level monitor grant is
never the path that authorizes.
- ResourceWorkspaceAgentDevcontainers {*}
Dbauthz call sites use the bare resource. Agent-side perms come
from system roles.
- ResourceTailnetCoordinator {*}
Dbauthz call sites use the bare resource. Tailnet ops are
granted to system / agent roles.
- ResourceReplicas {read}
Bare resource at the single call site in
enterprise/coderd/replicas.go; Member-level never fires.
Behavior-preserving: all eight grants were also dead under the
legacy allPermsExcept(...) wildcard. The rbac, dbauthz, coderd, and
enterprise/coderd test suites pass at the same scope verified for
the initial PR 1 commit.
1247 lines
45 KiB
Go
1247 lines
45 KiB
Go
package rbac
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
|
"github.com/coder/coder/v2/coderd/util/slice"
|
|
)
|
|
|
|
const (
|
|
owner string = "owner"
|
|
member string = "member"
|
|
templateAdmin string = "template-admin"
|
|
userAdmin string = "user-admin"
|
|
auditor string = "auditor"
|
|
agentsAccess string = "agents-access"
|
|
// customSiteRole is a placeholder for all custom site roles.
|
|
// This is used for what roles can assign other roles.
|
|
// TODO: Make this more dynamic to allow other roles to grant.
|
|
customSiteRole string = "custom-site-role"
|
|
customOrganizationRole string = "custom-organization-role"
|
|
|
|
orgAdmin string = "organization-admin"
|
|
orgMember string = "organization-member"
|
|
orgServiceAccount string = "organization-service-account"
|
|
orgAuditor string = "organization-auditor"
|
|
orgUserAdmin string = "organization-user-admin"
|
|
orgTemplateAdmin string = "organization-template-admin"
|
|
orgWorkspaceCreationBan string = "organization-workspace-creation-ban"
|
|
|
|
prebuildsOrchestrator string = "prebuilds-orchestrator"
|
|
)
|
|
|
|
func init() {
|
|
// Always load defaults
|
|
ReloadBuiltinRoles(nil)
|
|
}
|
|
|
|
// RoleIdentifiers is a list of user assignable role names. The role names must be
|
|
// in the builtInRoles map. Any non-user assignable roles will generate an
|
|
// error on Expand.
|
|
type RoleIdentifiers []RoleIdentifier
|
|
|
|
func (names RoleIdentifiers) Expand() ([]Role, error) {
|
|
return rolesByNames(names)
|
|
}
|
|
|
|
func (names RoleIdentifiers) Names() []RoleIdentifier {
|
|
return names
|
|
}
|
|
|
|
// RoleIdentifier contains both the name of the role, and any organizational scope.
|
|
// Both fields are required to be globally unique and identifiable.
|
|
type RoleIdentifier struct {
|
|
Name string
|
|
// OrganizationID is uuid.Nil for unscoped roles (aka deployment wide)
|
|
OrganizationID uuid.UUID
|
|
}
|
|
|
|
func (r RoleIdentifier) IsOrgRole() bool {
|
|
return r.OrganizationID != uuid.Nil
|
|
}
|
|
|
|
// RoleNameFromString takes a formatted string '<role_name>[:org_id]'.
|
|
func RoleNameFromString(input string) (RoleIdentifier, error) {
|
|
var role RoleIdentifier
|
|
|
|
arr := strings.Split(input, ":")
|
|
if len(arr) > 2 {
|
|
return role, xerrors.Errorf("too many colons in role name")
|
|
}
|
|
|
|
if len(arr) == 0 {
|
|
return role, xerrors.Errorf("empty string not a valid role")
|
|
}
|
|
|
|
if arr[0] == "" {
|
|
return role, xerrors.Errorf("role cannot be the empty string")
|
|
}
|
|
|
|
role.Name = arr[0]
|
|
|
|
if len(arr) == 2 {
|
|
orgID, err := uuid.Parse(arr[1])
|
|
if err != nil {
|
|
return role, xerrors.Errorf("%q not a valid uuid: %w", arr[1], err)
|
|
}
|
|
role.OrganizationID = orgID
|
|
}
|
|
return role, nil
|
|
}
|
|
|
|
func (r RoleIdentifier) String() string {
|
|
if r.OrganizationID != uuid.Nil {
|
|
return r.Name + ":" + r.OrganizationID.String()
|
|
}
|
|
return r.Name
|
|
}
|
|
|
|
func (r RoleIdentifier) UniqueName() string {
|
|
return r.String()
|
|
}
|
|
|
|
func (r *RoleIdentifier) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(r.String())
|
|
}
|
|
|
|
func (r *RoleIdentifier) UnmarshalJSON(data []byte) error {
|
|
var str string
|
|
err := json.Unmarshal(data, &str)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
v, err := RoleNameFromString(str)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*r = v
|
|
return nil
|
|
}
|
|
|
|
// The functions below ONLY need to exist for roles that are "defaulted" in some way.
|
|
// Any other roles (like auditor), can be listed and let the user select/assigned.
|
|
// Once we have a database implementation, the "default" roles can be defined on the
|
|
// site and orgs, and these functions can be removed.
|
|
|
|
func RoleOwner() RoleIdentifier { return RoleIdentifier{Name: owner} }
|
|
func CustomSiteRole() RoleIdentifier { return RoleIdentifier{Name: customSiteRole} }
|
|
func CustomOrganizationRole(orgID uuid.UUID) RoleIdentifier {
|
|
return RoleIdentifier{Name: customOrganizationRole, OrganizationID: orgID}
|
|
}
|
|
func RoleTemplateAdmin() RoleIdentifier { return RoleIdentifier{Name: templateAdmin} }
|
|
func RoleUserAdmin() RoleIdentifier { return RoleIdentifier{Name: userAdmin} }
|
|
func RoleMember() RoleIdentifier { return RoleIdentifier{Name: member} }
|
|
func RoleAuditor() RoleIdentifier { return RoleIdentifier{Name: auditor} }
|
|
func RoleAgentsAccess() string { return agentsAccess }
|
|
|
|
func RoleOrgAdmin() string {
|
|
return orgAdmin
|
|
}
|
|
|
|
func RoleOrgMember() string {
|
|
return orgMember
|
|
}
|
|
|
|
func RoleOrgServiceAccount() string {
|
|
return orgServiceAccount
|
|
}
|
|
|
|
func RoleOrgAuditor() string {
|
|
return orgAuditor
|
|
}
|
|
|
|
func RoleOrgUserAdmin() string {
|
|
return orgUserAdmin
|
|
}
|
|
|
|
func RoleOrgTemplateAdmin() string {
|
|
return orgTemplateAdmin
|
|
}
|
|
|
|
func RoleOrgWorkspaceCreationBan() string {
|
|
return orgWorkspaceCreationBan
|
|
}
|
|
|
|
// ScopedRoleOrgAdmin is the org role with the organization ID
|
|
func ScopedRoleOrgAdmin(organizationID uuid.UUID) RoleIdentifier {
|
|
return RoleIdentifier{Name: RoleOrgAdmin(), OrganizationID: organizationID}
|
|
}
|
|
|
|
// ScopedRoleOrgMember is the org role with the organization ID
|
|
func ScopedRoleOrgMember(organizationID uuid.UUID) RoleIdentifier {
|
|
return RoleIdentifier{Name: RoleOrgMember(), OrganizationID: organizationID}
|
|
}
|
|
|
|
func ScopedRoleOrgAuditor(organizationID uuid.UUID) RoleIdentifier {
|
|
return RoleIdentifier{Name: RoleOrgAuditor(), OrganizationID: organizationID}
|
|
}
|
|
|
|
func ScopedRoleOrgUserAdmin(organizationID uuid.UUID) RoleIdentifier {
|
|
return RoleIdentifier{Name: RoleOrgUserAdmin(), OrganizationID: organizationID}
|
|
}
|
|
|
|
func ScopedRoleOrgTemplateAdmin(organizationID uuid.UUID) RoleIdentifier {
|
|
return RoleIdentifier{Name: RoleOrgTemplateAdmin(), OrganizationID: organizationID}
|
|
}
|
|
|
|
func ScopedRoleOrgWorkspaceCreationBan(organizationID uuid.UUID) RoleIdentifier {
|
|
return RoleIdentifier{Name: RoleOrgWorkspaceCreationBan(), OrganizationID: organizationID}
|
|
}
|
|
|
|
func ScopedRoleAgentsAccess(organizationID uuid.UUID) RoleIdentifier {
|
|
return RoleIdentifier{Name: RoleAgentsAccess(), OrganizationID: organizationID}
|
|
}
|
|
|
|
func allPermsExcept(excepts ...Objecter) []Permission {
|
|
resources := AllResources()
|
|
var perms []Permission
|
|
skip := make(map[string]bool)
|
|
for _, e := range excepts {
|
|
skip[e.RBACObject().Type] = true
|
|
}
|
|
|
|
for _, r := range resources {
|
|
// Exceptions
|
|
if skip[r.RBACObject().Type] {
|
|
continue
|
|
}
|
|
// This should always be skipped.
|
|
if r.RBACObject().Type == ResourceWildcard.Type {
|
|
continue
|
|
}
|
|
// Owners can do everything else
|
|
perms = append(perms, Permission{
|
|
Negate: false,
|
|
ResourceType: r.RBACObject().Type,
|
|
Action: policy.WildcardSymbol,
|
|
})
|
|
}
|
|
return perms
|
|
}
|
|
|
|
// builtInRoles are just a hard coded set for now. Ideally we store these in
|
|
// the database. Right now they are functions because the org id should scope
|
|
// certain roles. When we store them in the database, each organization should
|
|
// create the roles that are assignable in the org. This isn't a hard problem to solve,
|
|
// it's just easier as a function right now.
|
|
//
|
|
// This map will be replaced by database storage defined by this ticket.
|
|
// https://github.com/coder/coder/issues/1194
|
|
var builtInRoles map[string]func(orgID uuid.UUID) Role
|
|
|
|
type RoleOptions struct {
|
|
NoOwnerWorkspaceExec bool
|
|
NoWorkspaceSharing bool
|
|
NoChatSharing bool
|
|
}
|
|
|
|
// ReservedRoleName exists because the database should only allow unique role
|
|
// names, but some roles are built in. So these names are reserved
|
|
func ReservedRoleName(name string) bool {
|
|
_, ok := builtInRoles[name]
|
|
return ok
|
|
}
|
|
|
|
// ReloadBuiltinRoles loads the static roles into the builtInRoles map.
|
|
// This can be called again with a different config to change the behavior.
|
|
//
|
|
// TODO: @emyrk This would be great if it was instanced to a coderd rather
|
|
// than a global. But that is a much larger refactor right now.
|
|
// Essentially we did not foresee different deployments needing slightly
|
|
// different role permissions.
|
|
func ReloadBuiltinRoles(opts *RoleOptions) {
|
|
if opts == nil {
|
|
opts = &RoleOptions{}
|
|
}
|
|
|
|
denyPermissions := []Permission{}
|
|
if opts.NoWorkspaceSharing {
|
|
denyPermissions = append(denyPermissions, Permission{
|
|
Negate: true,
|
|
ResourceType: ResourceWorkspace.Type,
|
|
Action: policy.ActionShare,
|
|
})
|
|
}
|
|
if opts.NoChatSharing {
|
|
denyPermissions = append(denyPermissions, Permission{
|
|
Negate: true,
|
|
ResourceType: ResourceChat.Type,
|
|
Action: policy.ActionShare,
|
|
})
|
|
}
|
|
|
|
ownerWorkspaceActions := ResourceWorkspace.AvailableActions()
|
|
if opts.NoOwnerWorkspaceExec {
|
|
// Remove ssh and application connect from the owner role. This
|
|
// prevents owners from have exec access to all workspaces.
|
|
ownerWorkspaceActions = slice.Omit(
|
|
ownerWorkspaceActions,
|
|
policy.ActionApplicationConnect, policy.ActionSSH,
|
|
)
|
|
}
|
|
|
|
// Static roles that never change should be allocated in a closure.
|
|
// This is to ensure these data structures are only allocated once and not
|
|
// on every authorize call. 'withCachedRegoValue' can be used as well to
|
|
// preallocate the rego value that is used by the rego eval engine.
|
|
ownerRole := Role{
|
|
Identifier: RoleOwner(),
|
|
DisplayName: "Owner",
|
|
Site: append(
|
|
// Workspace dormancy and workspace are omitted.
|
|
// Workspace is specifically handled based on the opts.NoOwnerWorkspaceExec.
|
|
// Owners can inspect and delete personal skills for operability and
|
|
// abuse handling, but cannot create or edit user-authored instructions.
|
|
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUserSecret, ResourceUserSkill, ResourceUsageEvent, ResourceBoundaryUsage, ResourceBoundaryLog, ResourceAiSeat),
|
|
// 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, policy.ActionUpdateAgent},
|
|
ResourceUserSkill.Type: {policy.ActionRead, policy.ActionDelete},
|
|
// 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},
|
|
// Owners can read all boundary logs. Delete is reserved for
|
|
// DBPurge only. Create is user-scoped (inherited from member).
|
|
ResourceBoundaryLog.Type: {policy.ActionRead},
|
|
})...,
|
|
),
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{},
|
|
}.withCachedRegoValue()
|
|
|
|
memberRole := Role{
|
|
Identifier: RoleMember(),
|
|
DisplayName: "Member",
|
|
Site: append(
|
|
Permissions(map[string][]policy.Action{
|
|
ResourceAssignRole.Type: {policy.ActionRead},
|
|
// All users can see OAuth2 provider applications.
|
|
ResourceOauth2App.Type: {policy.ActionRead},
|
|
ResourceWorkspaceProxy.Type: {policy.ActionRead},
|
|
}),
|
|
denyPermissions...,
|
|
),
|
|
User: append(
|
|
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceBoundaryLog, ResourceAibridgeInterception, ResourceChat, ResourceAiSeat),
|
|
Permissions(map[string][]policy.Action{
|
|
// Users cannot do create/update/delete on themselves, but they
|
|
// can read their own details.
|
|
ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
|
|
// Users can create provisioner daemons scoped to themselves.
|
|
ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
|
// Members can create and update AI Bridge interceptions but
|
|
// cannot read them back.
|
|
ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate},
|
|
// Workspace agents create boundary logs under their owner's
|
|
// identity. Create is user-scoped so agents can only write
|
|
// logs owned by their workspace owner.
|
|
// Read: owners and auditors. Delete: DBPurge only.
|
|
ResourceBoundaryLog.Type: {policy.ActionCreate},
|
|
})...,
|
|
),
|
|
ByOrgID: map[string]OrgPermissions{},
|
|
}.withCachedRegoValue()
|
|
|
|
auditorRole := Role{
|
|
Identifier: RoleAuditor(),
|
|
DisplayName: "Auditor",
|
|
Site: Permissions(map[string][]policy.Action{
|
|
ResourceAssignOrgRole.Type: {policy.ActionRead},
|
|
ResourceAuditLog.Type: {policy.ActionRead},
|
|
ResourceConnectionLog.Type: {policy.ActionRead},
|
|
// Allow auditors to see the resources that audit logs reflect.
|
|
ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights},
|
|
ResourceUser.Type: {policy.ActionRead},
|
|
ResourceGroup.Type: {policy.ActionRead},
|
|
ResourceGroupMember.Type: {policy.ActionRead},
|
|
ResourceOrganization.Type: {policy.ActionRead},
|
|
ResourceOrganizationMember.Type: {policy.ActionRead},
|
|
// Allow auditors to query deployment stats and insights.
|
|
ResourceDeploymentStats.Type: {policy.ActionRead},
|
|
ResourceDeploymentConfig.Type: {policy.ActionRead},
|
|
// Allow auditors to query AI Bridge interceptions.
|
|
ResourceAibridgeInterception.Type: {policy.ActionRead},
|
|
// Allow auditors to read boundary logs.
|
|
ResourceBoundaryLog.Type: {policy.ActionRead},
|
|
}),
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{},
|
|
}.withCachedRegoValue()
|
|
|
|
templateAdminRole := Role{
|
|
Identifier: RoleTemplateAdmin(),
|
|
DisplayName: "Template Admin",
|
|
Site: Permissions(map[string][]policy.Action{
|
|
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},
|
|
ResourceWorkspaceDormant.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
|
|
ResourceUser.Type: {policy.ActionRead},
|
|
ResourceGroup.Type: {policy.ActionRead},
|
|
ResourceGroupMember.Type: {policy.ActionRead},
|
|
ResourceOrganization.Type: {policy.ActionRead},
|
|
ResourceOrganizationMember.Type: {policy.ActionRead},
|
|
}),
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{},
|
|
}.withCachedRegoValue()
|
|
|
|
userAdminRole := Role{
|
|
Identifier: RoleUserAdmin(),
|
|
DisplayName: "User Admin",
|
|
Site: Permissions(map[string][]policy.Action{
|
|
ResourceAssignRole.Type: {policy.ActionAssign, policy.ActionUnassign, policy.ActionRead},
|
|
// Need organization assign as well to create users. At present, creating a user
|
|
// will always assign them to some organization.
|
|
ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionUnassign, policy.ActionRead},
|
|
ResourceUser.Type: {
|
|
policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete,
|
|
policy.ActionUpdatePersonal, policy.ActionReadPersonal,
|
|
},
|
|
ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
|
ResourceGroupMember.Type: {policy.ActionRead},
|
|
ResourceOrganization.Type: {policy.ActionRead},
|
|
// Full perms to manage org members
|
|
ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
|
// Manage org membership based on OIDC claims
|
|
ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate},
|
|
}),
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{},
|
|
}.withCachedRegoValue()
|
|
|
|
builtInRoles = map[string]func(orgID uuid.UUID) Role{
|
|
// admin grants all actions to all resources.
|
|
owner: func(_ uuid.UUID) Role {
|
|
return ownerRole
|
|
},
|
|
|
|
// member grants all actions to all resources owned by the user
|
|
member: func(_ uuid.UUID) Role {
|
|
return memberRole
|
|
},
|
|
|
|
// auditor provides all permissions required to effectively read and understand
|
|
// audit log events.
|
|
// TODO: Finish the auditor as we add resources.
|
|
auditor: func(_ uuid.UUID) Role {
|
|
return auditorRole
|
|
},
|
|
|
|
// templateAdmin grants all actions on templates, files,
|
|
// provisioner daemons, and prebuilt workspaces.
|
|
templateAdmin: func(_ uuid.UUID) Role {
|
|
return templateAdminRole
|
|
},
|
|
|
|
// userAdmin grants all actions on users, groups, roles,
|
|
// and organization membership.
|
|
userAdmin: func(_ uuid.UUID) Role {
|
|
return userAdminRole
|
|
},
|
|
|
|
// orgAdmin returns a role with all actions allows in a given
|
|
// organization scope.
|
|
orgAdmin: func(organizationID uuid.UUID) Role {
|
|
return Role{
|
|
Identifier: RoleIdentifier{Name: orgAdmin, OrganizationID: organizationID},
|
|
DisplayName: "Organization Admin",
|
|
Site: Permissions(map[string][]policy.Action{
|
|
// To assign organization members, we need to be able to read
|
|
// users at the site wide to know they exist.
|
|
ResourceUser.Type: {policy.ActionRead},
|
|
}),
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{
|
|
// Org admins should not have workspace exec perms.
|
|
organizationID.String(): {
|
|
Org: append(
|
|
allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret, ResourceBoundaryUsage, ResourceBoundaryLog, ResourceAiSeat),
|
|
Permissions(map[string][]policy.Action{
|
|
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
|
|
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
|
// 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},
|
|
})...,
|
|
),
|
|
Member: []Permission{},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
orgAuditor: func(organizationID uuid.UUID) Role {
|
|
return Role{
|
|
Identifier: RoleIdentifier{Name: orgAuditor, OrganizationID: organizationID},
|
|
DisplayName: "Organization Auditor",
|
|
Site: []Permission{},
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{
|
|
organizationID.String(): {
|
|
Org: Permissions(map[string][]policy.Action{
|
|
ResourceAuditLog.Type: {policy.ActionRead},
|
|
ResourceConnectionLog.Type: {policy.ActionRead},
|
|
// Allow auditors to see the resources that audit logs reflect.
|
|
ResourceTemplate.Type: {policy.ActionRead, policy.ActionViewInsights},
|
|
ResourceGroup.Type: {policy.ActionRead},
|
|
ResourceGroupMember.Type: {policy.ActionRead},
|
|
ResourceOrganization.Type: {policy.ActionRead},
|
|
ResourceOrganizationMember.Type: {policy.ActionRead},
|
|
}),
|
|
Member: []Permission{},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
orgUserAdmin: func(organizationID uuid.UUID) Role {
|
|
// Manages organization members and groups.
|
|
return Role{
|
|
Identifier: RoleIdentifier{Name: orgUserAdmin, OrganizationID: organizationID},
|
|
DisplayName: "Organization User Admin",
|
|
Site: Permissions(map[string][]policy.Action{
|
|
// To assign organization members, we need to be able to read
|
|
// users at the site wide to know they exist.
|
|
ResourceUser.Type: {policy.ActionRead},
|
|
}),
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{
|
|
organizationID.String(): {
|
|
Org: Permissions(map[string][]policy.Action{
|
|
// Assign, remove, and read roles in the organization.
|
|
ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionUnassign, policy.ActionRead},
|
|
ResourceOrganization.Type: {policy.ActionRead},
|
|
ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
|
ResourceGroup.Type: ResourceGroup.AvailableActions(),
|
|
ResourceGroupMember.Type: ResourceGroupMember.AvailableActions(),
|
|
ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate},
|
|
}),
|
|
Member: []Permission{},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
orgTemplateAdmin: func(organizationID uuid.UUID) Role {
|
|
// Manages organization members and groups.
|
|
return Role{
|
|
Identifier: RoleIdentifier{Name: orgTemplateAdmin, OrganizationID: organizationID},
|
|
DisplayName: "Organization Template Admin",
|
|
Site: []Permission{},
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{
|
|
organizationID.String(): {
|
|
Org: Permissions(map[string][]policy.Action{
|
|
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
|
|
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
|
|
ResourceWorkspace.Type: {policy.ActionRead},
|
|
ResourceWorkspaceDormant.Type: {policy.ActionRead},
|
|
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
|
|
// Assigning template perms requires this permission.
|
|
ResourceOrganization.Type: {policy.ActionRead},
|
|
ResourceOrganizationMember.Type: {policy.ActionRead},
|
|
ResourceGroup.Type: {policy.ActionRead},
|
|
ResourceGroupMember.Type: {policy.ActionRead},
|
|
// Since templates have to correlate with provisioners,
|
|
// the ability to create templates and provisioners has
|
|
// a lot of overlap.
|
|
ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
|
ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate},
|
|
}),
|
|
Member: []Permission{},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
// orgWorkspaceCreationBan prevents creating & deleting workspaces. This
|
|
// overrides any permissions granted by the org or user level. It accomplishes
|
|
// this by using negative permissions.
|
|
orgWorkspaceCreationBan: func(organizationID uuid.UUID) Role {
|
|
return Role{
|
|
Identifier: RoleIdentifier{Name: orgWorkspaceCreationBan, OrganizationID: organizationID},
|
|
DisplayName: "Organization Workspace Creation Ban",
|
|
Site: []Permission{},
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{
|
|
organizationID.String(): {
|
|
Org: []Permission{
|
|
{
|
|
Negate: true,
|
|
ResourceType: ResourceWorkspace.Type,
|
|
Action: policy.ActionCreate,
|
|
},
|
|
{
|
|
Negate: true,
|
|
ResourceType: ResourceWorkspace.Type,
|
|
Action: policy.ActionDelete,
|
|
},
|
|
{
|
|
Negate: true,
|
|
ResourceType: ResourceWorkspace.Type,
|
|
Action: policy.ActionCreateAgent,
|
|
},
|
|
{
|
|
Negate: true,
|
|
ResourceType: ResourceWorkspace.Type,
|
|
Action: policy.ActionDeleteAgent,
|
|
},
|
|
},
|
|
Member: []Permission{},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
// ActionDelete is intentionally excluded because hard-deletion goes through
|
|
// ResourceSystem in dbpurge.
|
|
agentsAccess: func(organizationID uuid.UUID) Role {
|
|
return Role{
|
|
Identifier: RoleIdentifier{Name: agentsAccess, OrganizationID: organizationID},
|
|
DisplayName: "Coder Agents User",
|
|
Site: []Permission{},
|
|
User: []Permission{},
|
|
ByOrgID: map[string]OrgPermissions{
|
|
organizationID.String(): {
|
|
Org: []Permission{},
|
|
Member: Permissions(map[string][]policy.Action{
|
|
ResourceChat.Type: {
|
|
policy.ActionCreate,
|
|
policy.ActionRead,
|
|
policy.ActionShare,
|
|
policy.ActionUpdate,
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
// assignRoles is a map of roles that can be assigned if a user has a given
|
|
// role.
|
|
// The first key is the actor role, the second is the roles they can assign.
|
|
//
|
|
// map[actor_role][assign_role]<can_assign>
|
|
var assignRoles = map[string]map[string]bool{
|
|
"system": {
|
|
owner: true,
|
|
auditor: true,
|
|
member: true,
|
|
orgAdmin: true,
|
|
orgMember: true,
|
|
orgAuditor: true,
|
|
orgUserAdmin: true,
|
|
orgTemplateAdmin: true,
|
|
orgWorkspaceCreationBan: true,
|
|
templateAdmin: true,
|
|
userAdmin: true,
|
|
customSiteRole: true,
|
|
customOrganizationRole: true,
|
|
agentsAccess: true,
|
|
},
|
|
owner: {
|
|
owner: true,
|
|
auditor: true,
|
|
member: true,
|
|
orgAdmin: true,
|
|
orgMember: true,
|
|
orgAuditor: true,
|
|
orgUserAdmin: true,
|
|
orgTemplateAdmin: true,
|
|
orgWorkspaceCreationBan: true,
|
|
templateAdmin: true,
|
|
userAdmin: true,
|
|
customSiteRole: true,
|
|
customOrganizationRole: true,
|
|
agentsAccess: true,
|
|
},
|
|
userAdmin: {
|
|
member: true,
|
|
orgMember: true,
|
|
agentsAccess: true,
|
|
},
|
|
orgAdmin: {
|
|
orgAdmin: true,
|
|
orgMember: true,
|
|
orgAuditor: true,
|
|
orgUserAdmin: true,
|
|
orgTemplateAdmin: true,
|
|
orgWorkspaceCreationBan: true,
|
|
customOrganizationRole: true,
|
|
agentsAccess: true,
|
|
},
|
|
orgUserAdmin: {
|
|
orgMember: true,
|
|
agentsAccess: true,
|
|
},
|
|
|
|
prebuildsOrchestrator: {
|
|
orgMember: true,
|
|
},
|
|
}
|
|
|
|
// ExpandableRoles is any type that can be expanded into a []Role. This is implemented
|
|
// as an interface so we can have RoleIdentifiers for user defined roles, and implement
|
|
// custom ExpandableRoles for system type users (eg autostart/autostop system role).
|
|
// We want a clear divide between the two types of roles so users have no codepath
|
|
// to interact or assign system roles.
|
|
//
|
|
// Note: We may also want to do the same thing with scopes to allow custom scope
|
|
// support unavailable to the user. Eg: Scope to a single resource.
|
|
type ExpandableRoles interface {
|
|
Expand() ([]Role, error)
|
|
// Names is for logging and tracing purposes, we want to know the human
|
|
// names of the expanded roles.
|
|
Names() []RoleIdentifier
|
|
}
|
|
|
|
// Permission is the format passed into the rego.
|
|
type Permission struct {
|
|
// Negate makes this a negative permission
|
|
Negate bool `json:"negate"`
|
|
ResourceType string `json:"resource_type"`
|
|
Action policy.Action `json:"action"`
|
|
}
|
|
|
|
func (perm Permission) Valid() error {
|
|
if perm.ResourceType == policy.WildcardSymbol {
|
|
// Wildcard is tricky to check. Just allow it.
|
|
return nil
|
|
}
|
|
|
|
resource, ok := policy.RBACPermissions[perm.ResourceType]
|
|
if !ok {
|
|
return xerrors.Errorf("invalid resource type %q", perm.ResourceType)
|
|
}
|
|
|
|
// Wildcard action is always valid
|
|
if perm.Action == policy.WildcardSymbol {
|
|
return nil
|
|
}
|
|
|
|
_, ok = resource.Actions[perm.Action]
|
|
if !ok {
|
|
return xerrors.Errorf("invalid action %q for resource %q", perm.Action, perm.ResourceType)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Role is a set of permissions at multiple levels:
|
|
// - Site permissions apply EVERYWHERE
|
|
// - Org permissions apply to EVERYTHING in a given ORG
|
|
// - User permissions apply to all resources the user owns
|
|
// - OrgMember permissions apply to resources in the given org that the user owns
|
|
// This is the type passed into the rego as a json payload.
|
|
// Users of this package should instead **only** use the role names, and
|
|
// this package will expand the role names into their json payloads.
|
|
type Role struct {
|
|
Identifier RoleIdentifier `json:"name"`
|
|
// DisplayName is used for UI purposes. If the role has no display name,
|
|
// that means the UI should never display it.
|
|
DisplayName string `json:"display_name"`
|
|
Site []Permission `json:"site"`
|
|
User []Permission `json:"user"`
|
|
// ByOrgID is a map of organization IDs to permissions. Grouping by
|
|
// organization makes roles easy to combine.
|
|
ByOrgID map[string]OrgPermissions `json:"by_org_id"`
|
|
|
|
// cachedRegoValue can be used to cache the rego value for this role.
|
|
// This is helpful for static roles that never change.
|
|
cachedRegoValue ast.Value
|
|
}
|
|
|
|
type OrgPermissions struct {
|
|
Org []Permission `json:"org"`
|
|
Member []Permission `json:"member"`
|
|
}
|
|
|
|
// Valid will check all it's permissions and ensure they are all correct
|
|
// according to the policy. This verifies every action specified make sense
|
|
// for the given resource.
|
|
func (role Role) Valid() error {
|
|
var errs []error
|
|
for _, perm := range role.Site {
|
|
if err := perm.Valid(); err != nil {
|
|
errs = append(errs, xerrors.Errorf("site: %w", err))
|
|
}
|
|
}
|
|
|
|
for orgID, orgPermissions := range role.ByOrgID {
|
|
for _, perm := range orgPermissions.Org {
|
|
if err := perm.Valid(); err != nil {
|
|
errs = append(errs, xerrors.Errorf("org=%q: org %w", orgID, err))
|
|
}
|
|
}
|
|
for _, perm := range orgPermissions.Member {
|
|
if err := perm.Valid(); err != nil {
|
|
errs = append(errs, xerrors.Errorf("org=%q: member: %w", orgID, err))
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, perm := range role.User {
|
|
if err := perm.Valid(); err != nil {
|
|
errs = append(errs, xerrors.Errorf("user: %w", err))
|
|
}
|
|
}
|
|
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
type Roles []Role
|
|
|
|
func (roles Roles) Expand() ([]Role, error) {
|
|
return roles, nil
|
|
}
|
|
|
|
func (roles Roles) Names() []RoleIdentifier {
|
|
names := make([]RoleIdentifier, 0, len(roles))
|
|
for _, r := range roles {
|
|
names = append(names, r.Identifier)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// CanAssignRole is a helper function that returns true if the user can assign
|
|
// the specified role. This also can be used for removing a role.
|
|
// This is a simple implementation for now.
|
|
func CanAssignRole(subjectHasRoles ExpandableRoles, assignedRole RoleIdentifier) bool {
|
|
// For CanAssignRole, we only care about the names of the roles.
|
|
roles := subjectHasRoles.Names()
|
|
|
|
for _, myRole := range roles {
|
|
if myRole.OrganizationID != uuid.Nil && myRole.OrganizationID != assignedRole.OrganizationID {
|
|
// Org roles only apply to the org they are assigned to.
|
|
continue
|
|
}
|
|
|
|
allowedAssignList, ok := assignRoles[myRole.Name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if allowedAssignList[assignedRole.Name] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RoleByName returns the permissions associated with a given role name.
|
|
// This allows just the role names to be stored and expanded when required.
|
|
//
|
|
// This function is exported so that the Display name can be returned to the
|
|
// api. We should maybe make an exported function that returns just the
|
|
// human-readable content of the Role struct (name + display name).
|
|
func RoleByName(name RoleIdentifier) (Role, error) {
|
|
roleFunc, ok := builtInRoles[name.Name]
|
|
if !ok {
|
|
// No role found
|
|
return Role{}, xerrors.Errorf("role %q not found", name.String())
|
|
}
|
|
|
|
// Ensure all org roles are properly scoped a non-empty organization id.
|
|
// This is just some defensive programming.
|
|
role := roleFunc(name.OrganizationID)
|
|
if len(role.ByOrgID) > 0 && name.OrganizationID == uuid.Nil {
|
|
return Role{}, xerrors.Errorf("expect a org id for role %q", name.String())
|
|
}
|
|
|
|
// This can happen if a custom role shares the same name as a built-in role.
|
|
// You could make an org role called "owner", and we should not return the
|
|
// owner role itself.
|
|
if name.OrganizationID != role.Identifier.OrganizationID {
|
|
return Role{}, xerrors.Errorf("role %q not found", name.String())
|
|
}
|
|
|
|
return role, nil
|
|
}
|
|
|
|
func rolesByNames(roleNames []RoleIdentifier) ([]Role, error) {
|
|
roles := make([]Role, 0, len(roleNames))
|
|
for _, n := range roleNames {
|
|
r, err := RoleByName(n)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("get role permissions: %w", err)
|
|
}
|
|
roles = append(roles, r)
|
|
}
|
|
return roles, nil
|
|
}
|
|
|
|
// OrganizationRoles lists all roles that can be applied to an organization user
|
|
// in the given organization. This is the list of available roles,
|
|
// and specific to an organization.
|
|
//
|
|
// This should be a list in a database, but until then we build
|
|
// the list from the builtins.
|
|
func OrganizationRoles(organizationID uuid.UUID) []Role {
|
|
var roles []Role
|
|
for _, roleF := range builtInRoles {
|
|
role := roleF(organizationID)
|
|
if role.Identifier.OrganizationID == organizationID {
|
|
roles = append(roles, role)
|
|
}
|
|
}
|
|
return roles
|
|
}
|
|
|
|
// SiteBuiltInRoles lists all roles that can be applied to a user.
|
|
// This is the list of available roles, and not specific to a user
|
|
//
|
|
// This should be a list in a database, but until then we build
|
|
// the list from the builtins.
|
|
func SiteBuiltInRoles() []Role {
|
|
var roles []Role
|
|
for _, roleF := range builtInRoles {
|
|
// Must provide some non-nil uuid to filter out org roles.
|
|
role := roleF(uuid.New())
|
|
if !role.Identifier.IsOrgRole() {
|
|
roles = append(roles, role)
|
|
}
|
|
}
|
|
return roles
|
|
}
|
|
|
|
// ChangeRoleSet is a helper function that finds the difference of 2 sets of
|
|
// roles. When setting a user's new roles, it is equivalent to adding and
|
|
// removing roles. This set determines the changes, so that the appropriate
|
|
// RBAC checks can be applied using "ActionCreate" and "ActionDelete" for
|
|
// "added" and "removed" roles respectively.
|
|
func ChangeRoleSet(from []RoleIdentifier, to []RoleIdentifier) (added []RoleIdentifier, removed []RoleIdentifier) {
|
|
return slice.SymmetricDifferenceFunc(from, to, func(a, b RoleIdentifier) bool {
|
|
return a.Name == b.Name && a.OrganizationID == b.OrganizationID
|
|
})
|
|
}
|
|
|
|
// Permissions is just a helper function to make building roles that list out resources
|
|
// and actions a bit easier.
|
|
func Permissions(perms map[string][]policy.Action) []Permission {
|
|
list := make([]Permission, 0, len(perms))
|
|
for k, actions := range perms {
|
|
for _, act := range actions {
|
|
list = append(list, Permission{
|
|
Negate: false,
|
|
ResourceType: k,
|
|
Action: act,
|
|
})
|
|
}
|
|
}
|
|
// Deterministic ordering of permissions
|
|
sort.Slice(list, func(i, j int) bool {
|
|
return list[i].ResourceType < list[j].ResourceType
|
|
})
|
|
return list
|
|
}
|
|
|
|
// DeduplicatePermissions removes duplicate Permission entries while preserving
|
|
// the original order of the first occurrence for deterministic evaluation.
|
|
func DeduplicatePermissions(perms []Permission) []Permission {
|
|
if len(perms) == 0 {
|
|
return perms
|
|
}
|
|
seen := make(map[string]struct{}, len(perms))
|
|
deduped := make([]Permission, 0, len(perms))
|
|
for _, perm := range perms {
|
|
key := perm.ResourceType + "\x00" + string(perm.Action) + "\x00" + strconv.FormatBool(perm.Negate)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
deduped = append(deduped, perm)
|
|
}
|
|
return deduped
|
|
}
|
|
|
|
// PermissionsEqual compares two permission slices as sets. Order and
|
|
// duplicate entries do not matter; it only checks that both slices
|
|
// contain the same unique permissions.
|
|
func PermissionsEqual(a, b []Permission) bool {
|
|
setA := make(map[Permission]struct{}, len(a))
|
|
for _, p := range a {
|
|
setA[p] = struct{}{}
|
|
}
|
|
|
|
setB := make(map[Permission]struct{}, len(b))
|
|
for _, p := range b {
|
|
if _, ok := setA[p]; !ok {
|
|
return false
|
|
}
|
|
setB[p] = struct{}{}
|
|
}
|
|
|
|
return len(setA) == len(setB)
|
|
}
|
|
|
|
// OrgSettings carries organization-level settings that affect system
|
|
// role permissions. It lives in the rbac package to avoid a cyclic
|
|
// dependency with the database package. Callers in rolestore map
|
|
// database.Organization fields onto this struct.
|
|
type OrgSettings struct {
|
|
ShareableWorkspaceOwners ShareableWorkspaceOwners
|
|
}
|
|
type ShareableWorkspaceOwners string
|
|
|
|
const (
|
|
ShareableWorkspaceOwnersNone ShareableWorkspaceOwners = "none"
|
|
ShareableWorkspaceOwnersEveryone ShareableWorkspaceOwners = "everyone"
|
|
ShareableWorkspaceOwnersServiceAccounts ShareableWorkspaceOwners = "service_accounts"
|
|
)
|
|
|
|
// OrgRolePermissions holds the two permission sets that make up a
|
|
// system role: org-wide permissions and member-scoped permissions.
|
|
type OrgRolePermissions struct {
|
|
Org []Permission
|
|
Member []Permission
|
|
}
|
|
|
|
// OrgMemberPermissions returns the permissions for the organization-member
|
|
// system role, which can vary based on the organization's workspace sharing
|
|
// settings.
|
|
func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {
|
|
// Organization-level permissions that all org members get.
|
|
orgPermMap := map[string][]policy.Action{
|
|
// All users can see provisioner daemons for workspace creation.
|
|
ResourceProvisionerDaemon.Type: {policy.ActionRead},
|
|
// All org members can read the organization.
|
|
ResourceOrganization.Type: {policy.ActionRead},
|
|
// Can read available roles.
|
|
ResourceAssignOrgRole.Type: {policy.ActionRead},
|
|
}
|
|
|
|
// In all modes of workspace sharing but `none`, members need to
|
|
// see other org members (including service accounts) to either
|
|
// share with them or get access to their shared workspaces,
|
|
// resolved through GET /users/{user}/workspace/{workspace}
|
|
if org.ShareableWorkspaceOwners != ShareableWorkspaceOwnersNone {
|
|
orgPermMap[ResourceOrganizationMember.Type] = []policy.Action{policy.ActionRead}
|
|
}
|
|
|
|
// When workspace sharing is open to members, they also need to
|
|
// see org groups to share with them.
|
|
if org.ShareableWorkspaceOwners == ShareableWorkspaceOwnersEveryone {
|
|
orgPermMap[ResourceGroup.Type] = []policy.Action{policy.ActionRead}
|
|
}
|
|
|
|
orgPerms := Permissions(orgPermMap)
|
|
|
|
if org.ShareableWorkspaceOwners == ShareableWorkspaceOwnersNone {
|
|
// Org-level negation blocks sharing on ANY workspace in the
|
|
// org. This overrides any positive permission from other
|
|
// roles, including org-admin.
|
|
orgPerms = append(orgPerms, Permission{
|
|
Negate: true,
|
|
ResourceType: ResourceWorkspace.Type,
|
|
Action: policy.ActionShare,
|
|
})
|
|
}
|
|
|
|
// Enumerate the per-member resources explicitly so new resources do
|
|
// not auto-grant to org members. Adding a resource to the codebase
|
|
// requires an explicit decision to expose it here.
|
|
//
|
|
// Member-level grants only fire when input.object.owner ==
|
|
// input.subject.id (see the org_member rule in
|
|
// coderd/rbac/policy.rego). Only resources whose RBACObject() calls
|
|
// WithOwner(...) at production call sites belong here; see the
|
|
// "Intentionally omitted" block at the bottom.
|
|
memberPerms := Permissions(map[string][]policy.Action{
|
|
// Workspace lifecycle on resources owned by this member.
|
|
ResourceWorkspace.Type: ResourceWorkspace.AvailableActions(),
|
|
|
|
// Dormant workspaces share the workspace action set minus the
|
|
// build, ssh, and exec actions.
|
|
ResourceWorkspaceDormant.Type: {
|
|
policy.ActionRead,
|
|
policy.ActionDelete,
|
|
policy.ActionCreate,
|
|
policy.ActionUpdate,
|
|
policy.ActionWorkspaceStop,
|
|
policy.ActionCreateAgent,
|
|
policy.ActionDeleteAgent,
|
|
policy.ActionUpdateAgent,
|
|
},
|
|
|
|
// Upload and read template files the member created during
|
|
// workspace build (File.RBACObject sets WithOwner(CreatedBy)).
|
|
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
|
|
|
|
// Tasks ride along with workspaces and are owner-scoped.
|
|
ResourceTask.Type: ResourceTask.AvailableActions(),
|
|
|
|
// Read-self group-membership record. GroupMember.RBACObject
|
|
// sets WithOwner to the user's own ID.
|
|
ResourceGroupMember.Type: {policy.ActionRead},
|
|
|
|
// Read-self org-member record.
|
|
ResourceOrganizationMember.Type: {policy.ActionRead},
|
|
|
|
// Members can create and update AI Bridge interceptions they
|
|
// initiate (dbauthz layer sets WithOwner(InitiatorID)) but
|
|
// cannot read them back. Chat access requires the
|
|
// agents-access role and is intentionally not granted here.
|
|
ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate},
|
|
|
|
// Own session tokens and workspace agent auth keys.
|
|
ResourceApiKey.Type: ResourceApiKey.AvailableActions(),
|
|
|
|
// User-scoped notification surfaces. All three resources are
|
|
// addressed by WithOwner(user_id) at the call sites.
|
|
ResourceNotificationMessage.Type: {policy.ActionRead, policy.ActionUpdate},
|
|
ResourceNotificationPreference.Type: ResourceNotificationPreference.AvailableActions(),
|
|
ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(),
|
|
|
|
// Intentionally omitted at Member scope (resources without an
|
|
// Owner field on their RBACObject; Member-level grants never
|
|
// fire for them). Listed here so a future maintainer who sees
|
|
// these dropped relative to the legacy allPermsExcept(...)
|
|
// wildcard does not "restore" them:
|
|
//
|
|
// - ResourceTemplate: templates have no owner. Org-member
|
|
// template.use is authorized via the ACL path
|
|
// (acl_group_list[org_owner] "Everyone" group, populated
|
|
// on each template's GroupACL).
|
|
// - ResourceGroup: groups have no owner. "Groups I'm a
|
|
// member of can read themselves" is granted via the
|
|
// per-group GroupACL.
|
|
// - ResourceWorkspaceProxy, ResourceProvisionerJobs,
|
|
// ResourceWorkspaceAgentResourceMonitor,
|
|
// ResourceWorkspaceAgentDevcontainers,
|
|
// ResourceTailnetCoordinator, ResourceReplicas: these
|
|
// resources have no DB model that sets Owner; all
|
|
// production call sites use the bare resource or
|
|
// .InOrg(...) only. Access for these flows through Org
|
|
// perms on the appropriate role (e.g. ProvisionerDaemon
|
|
// above), or through system / agent / template-admin
|
|
// roles defined elsewhere.
|
|
})
|
|
|
|
if org.ShareableWorkspaceOwners != ShareableWorkspaceOwnersEveryone {
|
|
memberPerms = append(memberPerms, Permission{
|
|
Negate: true,
|
|
ResourceType: ResourceWorkspace.Type,
|
|
Action: policy.ActionShare,
|
|
})
|
|
}
|
|
|
|
return OrgRolePermissions{Org: orgPerms, Member: memberPerms}
|
|
}
|
|
|
|
// OrgServiceAccountPermissions returns the permissions for the
|
|
// organization-service-account system role, which can vary based on
|
|
// the organization's workspace sharing settings.
|
|
func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions {
|
|
// Organization-level permissions that all org service accounts get.
|
|
orgPermMap := map[string][]policy.Action{
|
|
// All users can see provisioner daemons for workspace creation.
|
|
ResourceProvisionerDaemon.Type: {policy.ActionRead},
|
|
// All org members can read the organization.
|
|
ResourceOrganization.Type: {policy.ActionRead},
|
|
// Can read available roles.
|
|
ResourceAssignOrgRole.Type: {policy.ActionRead},
|
|
}
|
|
|
|
// When workspace sharing is enabled, service accounts need to see
|
|
// other org members and groups to share workspaces with them.
|
|
if org.ShareableWorkspaceOwners != ShareableWorkspaceOwnersNone {
|
|
orgPermMap[ResourceOrganizationMember.Type] = []policy.Action{policy.ActionRead}
|
|
orgPermMap[ResourceGroup.Type] = []policy.Action{policy.ActionRead}
|
|
}
|
|
|
|
orgPerms := Permissions(orgPermMap)
|
|
|
|
if org.ShareableWorkspaceOwners == ShareableWorkspaceOwnersNone {
|
|
// Org-level negation blocks sharing on ANY workspace in the
|
|
// org. If a service account has any other roles assigned,
|
|
// this negation will override any positive perms in them, too.
|
|
orgPerms = append(orgPerms, Permission{
|
|
Negate: true,
|
|
ResourceType: ResourceWorkspace.Type,
|
|
Action: policy.ActionShare,
|
|
})
|
|
}
|
|
|
|
// service account-scoped permissions (resources owned by the
|
|
// service account). Enumerated explicitly so new resources do not
|
|
// auto-grant to service accounts.
|
|
//
|
|
// Member-level grants only fire when input.object.owner ==
|
|
// input.subject.id (see the org_member rule in
|
|
// coderd/rbac/policy.rego). Only resources whose RBACObject() calls
|
|
// WithOwner(...) at production call sites belong here; see the
|
|
// "Intentionally omitted" block at the bottom.
|
|
memberPerms := Permissions(map[string][]policy.Action{
|
|
// Workspace lifecycle on resources owned by this service account.
|
|
ResourceWorkspace.Type: ResourceWorkspace.AvailableActions(),
|
|
|
|
// Dormant workspaces share the workspace action set minus the
|
|
// build, ssh, and exec actions.
|
|
ResourceWorkspaceDormant.Type: {
|
|
policy.ActionRead,
|
|
policy.ActionDelete,
|
|
policy.ActionCreate,
|
|
policy.ActionUpdate,
|
|
policy.ActionWorkspaceStop,
|
|
policy.ActionCreateAgent,
|
|
policy.ActionDeleteAgent,
|
|
policy.ActionUpdateAgent,
|
|
},
|
|
|
|
// Upload and read template files the service account created
|
|
// during workspace build (File.RBACObject sets
|
|
// WithOwner(CreatedBy)).
|
|
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
|
|
|
|
// Tasks ride along with workspaces and are owner-scoped.
|
|
ResourceTask.Type: ResourceTask.AvailableActions(),
|
|
|
|
// Read-self group-membership record. GroupMember.RBACObject
|
|
// sets WithOwner to the user's own ID.
|
|
ResourceGroupMember.Type: {policy.ActionRead},
|
|
|
|
// Read-self org-member record.
|
|
ResourceOrganizationMember.Type: {policy.ActionRead},
|
|
|
|
// Service accounts can create and update AI Bridge
|
|
// interceptions they initiate (dbauthz layer sets
|
|
// WithOwner(InitiatorID)) but cannot read them back. Chat
|
|
// access requires the agents-access role and is intentionally
|
|
// not granted here.
|
|
ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate},
|
|
|
|
// Own session tokens and workspace agent auth keys.
|
|
ResourceApiKey.Type: ResourceApiKey.AvailableActions(),
|
|
|
|
// User-scoped notification surfaces. All three resources are
|
|
// addressed by WithOwner(user_id) at the call sites.
|
|
ResourceNotificationMessage.Type: {policy.ActionRead, policy.ActionUpdate},
|
|
ResourceNotificationPreference.Type: ResourceNotificationPreference.AvailableActions(),
|
|
ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(),
|
|
|
|
// Intentionally omitted at Member scope. See
|
|
// OrgMemberPermissions above for the rationale; the service
|
|
// account role mirrors the same partition.
|
|
})
|
|
|
|
return OrgRolePermissions{Org: orgPerms, Member: memberPerms}
|
|
}
|