chore: refactor roles to support multiple permission sets scoped by org id (#20186)

In preparation for adding the "member" permission level, which will also
be grouped by org ID, do a bit of a refactor to make room for it and the
existing "org" level to live in the same `map`
This commit is contained in:
ケイラ
2025-10-09 11:08:34 -06:00
committed by GitHub
parent 6213b30f10
commit caeff49aba
15 changed files with 235 additions and 204 deletions
+116 -101
View File
@@ -282,8 +282,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...),
Org: map[string][]Permission{},
User: []Permission{},
User: []Permission{},
ByOrgID: map[string]OrgPermissions{},
}.withCachedRegoValue()
memberRole := Role{
@@ -295,7 +295,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceOauth2App.Type: {policy.ActionRead},
ResourceWorkspaceProxy.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceUser, ResourceOrganizationMember),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build, ssh, or exec
@@ -309,6 +308,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
})...,
),
ByOrgID: map[string]OrgPermissions{},
}.withCachedRegoValue()
auditorRole := Role{
@@ -331,8 +331,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Allow auditors to query aibridge interceptions.
ResourceAibridgeInterception.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
User: []Permission{},
ByOrgID: map[string]OrgPermissions{},
}.withCachedRegoValue()
templateAdminRole := Role{
@@ -354,8 +354,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
ResourceOrganization.Type: {policy.ActionRead},
ResourceOrganizationMember.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
User: []Permission{},
ByOrgID: map[string]OrgPermissions{},
}.withCachedRegoValue()
userAdminRole := Role{
@@ -378,8 +378,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Manage org membership based on OIDC claims
ResourceIdpsyncSettings.Type: {policy.ActionRead, policy.ActionUpdate},
}),
Org: map[string][]Permission{},
User: []Permission{},
User: []Permission{},
ByOrgID: map[string]OrgPermissions{},
}.withCachedRegoValue()
builtInRoles = map[string]func(orgID uuid.UUID) Role{
@@ -419,18 +419,20 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// users at the site wide to know they exist.
ResourceUser.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{
// Org admins should not have workspace exec perms.
organizationID.String(): append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret), Permissions(map[string][]policy.Action{
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
// PrebuiltWorkspaces are a subset of Workspaces.
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...),
},
User: []Permission{},
ByOrgID: map[string]OrgPermissions{
// Org admins should not have workspace exec perms.
organizationID.String(): {
Org: append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceAssignRole, ResourceUserSecret), Permissions(map[string][]policy.Action{
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent},
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
// PrebuiltWorkspaces are a subset of Workspaces.
// Explicitly setting PrebuiltWorkspace permissions for clarity.
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
ResourcePrebuiltWorkspace.Type: {policy.ActionUpdate, policy.ActionDelete},
})...),
},
},
}
},
@@ -440,18 +442,20 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Identifier: RoleIdentifier{Name: orgMember, OrganizationID: organizationID},
DisplayName: "",
Site: []Permission{},
Org: map[string][]Permission{
organizationID.String(): Permissions(map[string][]policy.Action{
// All users can see the 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},
}),
User: []Permission{},
ByOrgID: map[string]OrgPermissions{
organizationID.String(): {
Org: Permissions(map[string][]policy.Action{
// All users can see the 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},
}),
},
},
User: []Permission{},
}
},
orgAuditor: func(organizationID uuid.UUID) Role {
@@ -459,19 +463,21 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Identifier: RoleIdentifier{Name: orgAuditor, OrganizationID: organizationID},
DisplayName: "Organization Auditor",
Site: []Permission{},
Org: map[string][]Permission{
organizationID.String(): 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},
}),
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},
}),
},
},
User: []Permission{},
}
},
orgUserAdmin: func(organizationID uuid.UUID) Role {
@@ -484,18 +490,20 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// users at the site wide to know they exist.
ResourceUser.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{
organizationID.String(): 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},
}),
},
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},
}),
},
},
}
},
orgTemplateAdmin: func(organizationID uuid.UUID) Role {
@@ -504,25 +512,27 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Identifier: RoleIdentifier{Name: orgTemplateAdmin, OrganizationID: organizationID},
DisplayName: "Organization Template Admin",
Site: []Permission{},
Org: map[string][]Permission{
organizationID.String(): Permissions(map[string][]policy.Action{
ResourceTemplate.Type: ResourceTemplate.AvailableActions(),
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
ResourceWorkspace.Type: {policy.ActionRead},
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},
}),
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},
}),
},
},
User: []Permission{},
}
},
// orgWorkspaceCreationBan prevents creating & deleting workspaces. This
@@ -533,31 +543,33 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
Identifier: RoleIdentifier{Name: orgWorkspaceCreationBan, OrganizationID: organizationID},
DisplayName: "Organization Workspace Creation Ban",
Site: []Permission{},
Org: map[string][]Permission{
User: []Permission{},
ByOrgID: map[string]OrgPermissions{
organizationID.String(): {
{
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,
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,
},
},
},
},
User: []Permission{},
}
},
}
@@ -680,17 +692,20 @@ type Role struct {
// that means the UI should never display it.
DisplayName string `json:"display_name"`
Site []Permission `json:"site"`
// Org is a map of orgid to permissions. We represent orgid as a string.
// We scope the organizations in the role so we can easily combine all the
// roles.
Org map[string][]Permission `json:"org"`
User []Permission `json:"user"`
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"`
}
// 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.
@@ -702,8 +717,8 @@ func (role Role) Valid() error {
}
}
for orgID, permissions := range role.Org {
for _, perm := range permissions {
for orgID, orgPermissions := range role.ByOrgID {
for _, perm := range orgPermissions.Org {
if err := perm.Valid(); err != nil {
errs = append(errs, xerrors.Errorf("org=%q: %w", orgID, err))
}
@@ -774,7 +789,7 @@ func RoleByName(name RoleIdentifier) (Role, error) {
// 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.Org) > 0 && name.OrganizationID == uuid.Nil {
if len(role.ByOrgID) > 0 && name.OrganizationID == uuid.Nil {
return Role{}, xerrors.Errorf("expect a org id for role %q", name.String())
}