mirror of
https://github.com/coder/coder.git
synced 2026-06-06 14:38:23 +00:00
dba9f68b11
_Disclaimer:_ _produced_ _by_ _Claude_ _Opus_ _4\.6,_ _reviewed_ _by_ _me._ **This is a breaking change.** Users who are not have `owner` or sitewide `auditor` roles will no longer be able to view interceptions. Regular users should not need to view this information; in fact, it could be used by a malicious insider to see what information we track and don't track to exfiltrate data or perform actions unobserved. --- Changed authorization for AI Bridge interception-related operations from system-level permissions to resource-specific permissions. The following functions now authorize against `rbac.ResourceAibridgeInterception` instead of `rbac.ResourceSystem`: - `ListAIBridgeTokenUsagesByInterceptionIDs` - `ListAIBridgeToolUsagesByInterceptionIDs` - `ListAIBridgeUserPromptsByInterceptionIDs` Updated RBAC roles to grant AI Bridge interception permissions: - **User/Member roles**: Can create and update AI Bridge interceptions but cannot read them back - **Service accounts**: Same create/update permissions without read access - **Owners/Auditors**: Retain full read access to all interceptions Removed system-level authorization bypass in `populatedAndConvertAIBridgeInterceptions` function, allowing proper resource-level authorization checks. Updated tests to reflect the new permission model where members cannot view AI Bridge interceptions, even their own, while owners and auditors maintain full visibility.
1116 lines
38 KiB
Go
1116 lines
38 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"
|
|
// 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 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 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),
|
|
// 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, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceAibridgeInterception),
|
|
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: func(_ uuid.UUID) Role {
|
|
return templateAdminRole
|
|
},
|
|
|
|
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),
|
|
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{},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
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,
|
|
},
|
|
userAdmin: {
|
|
member: true,
|
|
orgMember: true,
|
|
},
|
|
orgAdmin: {
|
|
orgAdmin: true,
|
|
orgMember: true,
|
|
orgAuditor: true,
|
|
orgUserAdmin: true,
|
|
orgTemplateAdmin: true,
|
|
orgWorkspaceCreationBan: true,
|
|
customOrganizationRole: true,
|
|
},
|
|
orgUserAdmin: {
|
|
orgMember: 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,
|
|
),
|
|
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,
|
|
),
|
|
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}
|
|
}
|