Files
coder/coderd/rbac/roles.go
T

1162 lines
40 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
}
// 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,
})
}
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 cannot access other users' secrets.
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUserSecret, ResourceUsageEvent, ResourceBoundaryUsage, 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},
// PrebuiltWorkspaces are a subset of Workspaces.
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...,
),
User: []Permission{},
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, 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},
})...,
),
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},
}),
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},
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, 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},
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{},
},
},
}
},
// agentsAccess grants org members permission to create, read, and
// update chats. ActionDelete is intentionally excluded: no dbauthz
// function checks it on ResourceChat. Hard-deletion goes through
// ResourceSystem (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.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,
})
}
// Uses allPermsExcept to automatically include permissions for new resources.
memberPerms := append(
allPermsExcept(
ResourceWorkspaceDormant,
ResourcePrebuiltWorkspace,
ResourceUser,
ResourceOrganizationMember,
ResourceAibridgeInterception,
// Chat access requires the agents-access role.
ResourceChat,
),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build,
// ssh, or exec.
ResourceWorkspaceDormant.Type: {
policy.ActionRead,
policy.ActionDelete,
policy.ActionCreate,
policy.ActionUpdate,
policy.ActionWorkspaceStop,
policy.ActionCreateAgent,
policy.ActionDeleteAgent,
policy.ActionUpdateAgent,
},
// Can read their own organization member record.
ResourceOrganizationMember.Type: {
policy.ActionRead,
},
// Members can create and update AI Bridge interceptions but
// cannot read them back.
ResourceAibridgeInterception.Type: {
policy.ActionCreate,
policy.ActionUpdate,
},
})...,
)
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). Uses allPermsExcept to automatically include
// permissions for new resources.
memberPerms := append(
allPermsExcept(
ResourceWorkspaceDormant,
ResourcePrebuiltWorkspace,
ResourceUser,
ResourceOrganizationMember,
ResourceAibridgeInterception,
// Chat access requires the agents-access role.
ResourceChat,
),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build,
// ssh, or exec.
ResourceWorkspaceDormant.Type: {
policy.ActionRead,
policy.ActionDelete,
policy.ActionCreate,
policy.ActionUpdate,
policy.ActionWorkspaceStop,
policy.ActionCreateAgent,
policy.ActionDeleteAgent,
policy.ActionUpdateAgent,
},
// Can read their own organization member record.
ResourceOrganizationMember.Type: {
policy.ActionRead,
},
// Service accounts can create and update AI Bridge
// interceptions but cannot read them back.
ResourceAibridgeInterception.Type: {
policy.ActionCreate,
policy.ActionUpdate,
},
})...,
)
return OrgRolePermissions{Org: orgPerms, Member: memberPerms}
}