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 '[: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] 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}, // Create and read user-scoped provisioner daemons. The Upsert // path in dbauthz sets WithOwner(tag_owner) when scope=user, so // members can run their own daemons. Read is granted for // symmetry with workspace ownership: members can inspect // daemons they spawned even though no production call site // currently uses the member-scope read path (read on the bare // InOrg object continues to require Org-level perms). ResourceProvisionerDaemon.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}, // Create and read user-scoped provisioner daemons. The Upsert // path in dbauthz sets WithOwner(tag_owner) when scope=user, so // service accounts can run their own daemons. Read is granted // for symmetry with workspace ownership: service accounts can // inspect daemons they spawned even though no production call // site currently uses the member-scope read path (read on the // bare InOrg object continues to require Org-level perms). ResourceProvisionerDaemon.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} }