Permission Matrix & RBAC (Role-Based Access Control)
OEC.SH implements a comprehensive role-based access control system with fine-grained permissions, user-specific overrides, and Redis-cached abilities for optimal performance.
Overview
The Permission Matrix system provides:
- 3-tier permission hierarchy: Portal → Organization → Project
- 55+ granular permissions with
resource.actionnaming convention - 9 system roles with pre-configured permission mappings
- Custom roles for organization-specific needs
- User permission overrides for temporary elevated/restricted access
- Redis caching with 5-minute TTL for performance
- SSE events for real-time permission invalidation
- Complete audit trail for compliance and security
This system enables least-privilege access control, separation of duties, and flexible permission management across the entire platform.
Permission Hierarchy
3-Tier System
Permissions are organized hierarchically to match the platform's resource structure:
┌─────────────────────────────────────────┐
│ PORTAL (Platform) │
│ - Users, Organizations, Servers │
│ - Platform Settings, Permissions │
│ - Odoo Versions, Git Connections │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ ORGANIZATION (Tenant) │
│ - Members, Projects, Servers │
│ - Storage, Backups, Billing │
│ - DNS, Domains, Git, Addon Repos │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ PROJECT (Application) │
│ - Environments, Deployments │
│ - Backups, Domains │
│ - Repositories, Team Members │
└─────────────────────────────────────────┘Scope Inheritance
- Portal permissions apply platform-wide (system administrators only)
- Organization permissions apply within a specific organization
- Project permissions apply within a specific project
- Organization owners/admins inherit Project Admin access to all projects
- Portal admins bypass permission checks (audited)
Permission Naming Convention
All permissions follow the format: {scope}.{resource}.{action}
Components
- Scope:
portal,org, orproject - Resource: Entity being accessed (e.g.,
members,projects,backups) - Action: Operation being performed (e.g.,
list,create,update,delete)
Examples
portal.users.create → Create platform users
org.members.invite → Invite organization members
org.projects.delete → Delete organization projects
project.environments.deploy → Deploy to project environments
project.backups.restore → Restore project backupsAction Types
| Action | Description | Typical Use |
|---|---|---|
list | View a list of resources | GET /api/resources |
view | View details of a resource | GET /api/resources/:id |
create | Create new resources | POST /api/resources |
update | Modify existing resources | PUT/PATCH /api/resources/:id |
delete | Delete resources | DELETE /api/resources/:id |
manage | Full CRUD access | Admin operations |
deploy | Deployment operations | Environment deployments |
restart | Restart operations | Container restarts |
stop | Stop operations | Container stops |
shell | Shell/terminal access | Container exec |
logs | View logs | Log streaming |
config | Configuration access | Edit configs |
restore | Restore operations | Backup restores |
download | Download access | Backup downloads |
roles_update | Role assignment | Change member roles |
Complete Permission Matrix
Portal Permissions (15)
Platform-wide permissions for system administrators.
Users (5 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
portal.users.list | View Users | View list of all platform users | No |
portal.users.create | Create Users | Create new platform users | No |
portal.users.update | Edit Users | Modify user details and roles | No |
portal.users.status | Toggle User Status | Activate or deactivate user accounts | Yes |
portal.users.delete | Delete Users | Permanently delete users from the platform | Yes |
Organizations (3 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
portal.organizations.list | View Organizations | View all organizations on the platform | No |
portal.organizations.create | Create Organizations | Create new organizations | No |
portal.organizations.delete | Delete Organizations | Permanently delete organizations and all their data | Yes |
Infrastructure (1 permission)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
portal.servers.manage | Manage Servers | Full server management at platform level | Yes |
Security (1 permission)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
portal.permissions.manage | Manage Permissions | Configure roles and permissions system-wide | Yes |
Settings (2 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
portal.settings.view | View Platform Settings | View platform configuration | No |
portal.settings.update | Edit Platform Settings | Modify platform configuration | Yes |
Platform (1 permission)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
portal.odoo_versions.manage | Manage Odoo Versions | Add, update, or remove supported Odoo versions | No |
Integrations (2 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
portal.git_connections.manage | Manage Platform Git Connections | Configure platform-level Git integrations | No |
portal.addon_repos.manage | Manage Platform Addon Repos | Configure platform-level addon repositories | No |
Organization Permissions (37)
Organization-scoped permissions for tenant management.
Team (4 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.members.list | View Members | View organization team members | No |
org.members.invite | Invite Members | Invite new members to the organization | No |
org.members.remove | Remove Members | Remove members from the organization | Yes |
org.members.roles.update | Change Member Roles | Modify roles assigned to members | Yes |
Projects (4 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.projects.list | View Projects | View organization projects | No |
org.projects.create | Create Projects | Create new projects in the organization | No |
org.projects.update | Edit Projects | Modify project settings | No |
org.projects.delete | Delete Projects | Permanently delete projects and all environments | Yes |
Infrastructure (4 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.servers.list | View Servers | View organization servers | No |
org.servers.create | Add Servers | Add new servers to the organization | No |
org.servers.update | Edit Servers | Modify server configuration | No |
org.servers.delete | Remove Servers | Remove servers from the organization | Yes |
Storage (4 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.storage.list | View Storage Configs | View backup storage configurations | No |
org.storage.create | Create Storage Config | Add new storage providers for backups | No |
org.storage.update | Edit Storage Config | Modify storage provider settings | No |
org.storage.delete | Delete Storage Config | Remove storage configurations | Yes |
Backups (5 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.backups.list | View Backups | View all organization backups | No |
org.backups.create | Create Backups | Initiate backup operations | No |
org.backups.restore | Restore Backups | Restore data from backups | Yes |
org.backups.download | Download Backups | Download backup files from storage | No |
org.backups.delete | Delete Backups | Permanently delete backup files | Yes |
Integrations (4 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.git.list | View Git Connections | View organization Git integrations | No |
org.git.manage | Manage Git Connections | Configure organization Git integrations | No |
org.addon_repos.list | View Addon Repos | View organization addon repositories | No |
org.addon_repos.manage | Manage Addon Repos | Configure organization addon repositories | No |
Billing (2 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.billing.view | View Billing | View invoices and subscription details | No |
org.billing.manage | Manage Billing | Update payment methods and subscriptions | Yes |
Settings (2 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.settings.view | View Settings | View organization settings | No |
org.settings.update | Edit Settings | Modify organization settings | No |
Audit (1 permission)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.audit.view | View Audit Logs | View organization activity logs | No |
DNS (2 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.dns.list | View DNS Configs | View DNS provider configurations | No |
org.dns.manage | Manage DNS Configs | Configure DNS providers | No |
Domains (3 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.domains.list | View Domains | View organization domains | No |
org.domains.create | Add Domains | Add new domains to organization | No |
org.domains.delete | Remove Domains | Remove domains from organization | Yes |
Security (2 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
org.roles.view | View Roles | View organization roles and permissions | No |
org.roles.manage | Manage Roles | Create, edit, and delete organization roles | Yes |
Project Permissions (22)
Project-scoped permissions for application management.
General (2 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
project.view | View Project | Access project overview and details | No |
project.settings.update | Edit Project Settings | Modify project configuration | No |
Environments (9 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
project.environments.list | View Environments | View project environments | No |
project.environments.create | Create Environments | Create new environments | No |
project.environments.delete | Delete Environments | Permanently delete environments | Yes |
project.environments.deploy | Deploy | Deploy code changes to environments | No |
project.environments.restart | Restart | Restart environment containers | No |
project.environments.stop | Stop | Stop running environments | Yes |
project.environments.shell | Shell Access | Access environment command line | Yes |
project.environments.logs | View Logs | View environment logs | No |
project.environments.config | Edit Config | Modify environment configuration | No |
Backups (5 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
project.backups.list | View Backups | View project backups | No |
project.backups.create | Create Backups | Create new backups | No |
project.backups.restore | Restore Backups | Restore from backups | Yes |
project.backups.download | Download Backups | Download backup files | No |
project.backups.delete | Delete Backups | Delete backup files | Yes |
Domains (3 permissions)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
project.domains.list | View Domains | View project domains | No |
project.domains.create | Add Domains | Add new domains to environments | No |
project.domains.delete | Remove Domains | Remove domains from environments | Yes |
Team (1 permission)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
project.members.manage | Manage Project Members | Add/remove project-level members | Yes |
Integrations (1 permission)
| Code | Display Name | Description | Dangerous |
|---|---|---|---|
project.repos.manage | Manage Repositories | Configure project repositories | No |
System Roles
Portal Roles (2)
Platform-level roles for system administration.
Portal Admin
- Slug:
portal-admin - Scope: Portal
- Color: Red
- Icon: ShieldCheck
- Description: Full platform administration access
- Permissions: ALL portal permissions (15)
- portal.users.* (all user permissions)
- portal.organizations.* (all organization permissions)
- portal.servers.manage
- portal.permissions.manage
- portal.settings.* (all settings permissions)
- portal.odoo_versions.manage
- portal.git_connections.manage
- portal.addon_repos.manage
Security Note: Portal admins bypass permission checks but all actions are audited for compliance.
Portal Manager
- Slug:
portal-manager - Scope: Portal
- Color: Orange
- Icon: Shield
- Description: Platform management with limited destructive access
- Permissions: 9 portal permissions
- portal.users.list
- portal.users.create
- portal.users.update
- portal.organizations.list
- portal.organizations.create
- portal.settings.view
- portal.odoo_versions.manage
- portal.git_connections.manage
- portal.addon_repos.manage
Excluded: Cannot delete users/organizations, manage permissions, or update platform settings.
Organization Roles (4)
Tenant-level roles for organization management.
Owner
- Slug:
owner - Scope: Organization
- Color: Purple
- Icon: Crown
- Description: Full organization access including billing and deletion
- Permissions: ALL organization permissions (37)
- org.members.* (all member permissions)
- org.projects.* (all project permissions)
- org.servers.* (all server permissions)
- org.storage.* (all storage permissions)
- org.backups.* (all backup permissions)
- org.git.* (all git permissions)
- org.addon_repos.* (all addon repo permissions)
- org.billing.* (all billing permissions)
- org.settings.* (all settings permissions)
- org.audit.view
- org.dns.* (all DNS permissions)
- org.domains.* (all domain permissions)
- org.roles.* (all role permissions)
- Inherited: Project Admin access to all projects in organization
Use Case: Organization creator, primary account holder.
Admin
- Slug:
admin - Scope: Organization
- Color: Blue
- Icon: ShieldCheck
- Description: Organization administration without billing
- Permissions: 36 organization permissions (all except
org.billing.manage)- org.members.* (all member permissions)
- org.projects.* (all project permissions)
- org.servers.* (all server permissions)
- org.storage.* (all storage permissions)
- org.backups.* (all backup permissions)
- org.git.* (all git permissions)
- org.addon_repos.* (all addon repo permissions)
- org.billing.view (can view, not manage)
- org.settings.* (all settings permissions)
- org.audit.view
- org.dns.* (all DNS permissions)
- org.domains.* (all domain permissions)
- org.roles.* (all role permissions)
- Inherited: Project Admin access to all projects in organization
Use Case: Trusted administrators who manage day-to-day operations without billing access.
Developer
- Slug:
developer - Scope: Organization
- Color: Green
- Icon: Code
- Description: Project and environment management
- Permissions: 17 organization permissions (read-write on projects, read-only on infrastructure)
- org.members.list
- org.projects.list
- org.projects.create
- org.projects.update
- org.servers.list
- org.storage.list
- org.backups.list
- org.backups.create
- org.backups.download
- org.git.list
- org.addon_repos.list
- org.settings.view
- org.audit.view
- org.dns.list
- org.domains.list
- org.roles.view
- Inherited: Project Developer access to all projects in organization
Excluded: Cannot delete projects, modify servers/storage, invite/remove members, manage billing.
Use Case: Development team members who create and manage projects.
Viewer
- Slug:
viewer - Scope: Organization
- Color: Gray
- Icon: Eye
- Description: Read-only access to organization resources
- Permissions: 13 organization permissions (all list/view only)
- org.members.list
- org.projects.list
- org.servers.list
- org.storage.list
- org.backups.list
- org.git.list
- org.addon_repos.list
- org.settings.view
- org.dns.list
- org.domains.list
- org.roles.view
- Inherited: Project Viewer access to all projects in organization
Excluded: Cannot create, modify, or delete any resources.
Use Case: Auditors, stakeholders, read-only access for reporting.
Project Roles (3)
Application-level roles for project management.
Project Admin
- Slug:
project-admin - Scope: Project
- Color: Blue
- Icon: ShieldCheck
- Description: Full project access
- Permissions: ALL project permissions (22)
- project.view
- project.settings.update
- project.environments.* (all environment permissions)
- project.backups.* (all backup permissions)
- project.domains.* (all domain permissions)
- project.members.manage
- project.repos.manage
Use Case: Project leads, application owners.
Project Developer
- Slug:
project-developer - Scope: Project
- Color: Green
- Icon: Code
- Description: Development access without destructive operations
- Permissions: 15 project permissions (development workflows)
- project.view
- project.settings.update
- project.environments.list
- project.environments.create
- project.environments.deploy
- project.environments.restart
- project.environments.logs
- project.environments.config
- project.backups.list
- project.backups.create
- project.backups.download
- project.domains.list
- project.domains.create
- project.repos.manage
Excluded: Cannot delete environments, stop environments, access shell, restore backups, delete backups/domains, manage members.
Use Case: Developers who deploy and manage environments.
Project Viewer
- Slug:
project-viewer - Scope: Project
- Color: Gray
- Icon: Eye
- Description: Read-only project access
- Permissions: 5 project permissions (view only)
- project.view
- project.environments.list
- project.environments.logs
- project.backups.list
- project.domains.list
Excluded: Cannot create, modify, deploy, or delete any resources.
Use Case: QA testers, support team, read-only monitoring.
Role Permission Mapping
Complete Matrix
| Permission | Portal Admin | Portal Manager | Owner | Admin | Developer | Viewer | Project Admin | Project Developer | Project Viewer |
|---|---|---|---|---|---|---|---|---|---|
| Portal: Users | |||||||||
| portal.users.list | ✓ | ✓ | |||||||
| portal.users.create | ✓ | ✓ | |||||||
| portal.users.update | ✓ | ✓ | |||||||
| portal.users.status | ✓ | ||||||||
| portal.users.delete | ✓ | ||||||||
| Portal: Organizations | |||||||||
| portal.organizations.list | ✓ | ✓ | |||||||
| portal.organizations.create | ✓ | ✓ | |||||||
| portal.organizations.delete | ✓ | ||||||||
| Portal: Infrastructure | |||||||||
| portal.servers.manage | ✓ | ||||||||
| Portal: Security | |||||||||
| portal.permissions.manage | ✓ | ||||||||
| Portal: Settings | |||||||||
| portal.settings.view | ✓ | ✓ | |||||||
| portal.settings.update | ✓ | ||||||||
| Portal: Platform | |||||||||
| portal.odoo_versions.manage | ✓ | ✓ | |||||||
| Portal: Integrations | |||||||||
| portal.git_connections.manage | ✓ | ✓ | |||||||
| portal.addon_repos.manage | ✓ | ✓ | |||||||
| Org: Team | |||||||||
| org.members.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.members.invite | ✓ | ✓ | |||||||
| org.members.remove | ✓ | ✓ | |||||||
| org.members.roles.update | ✓ | ✓ | |||||||
| Org: Projects | |||||||||
| org.projects.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.projects.create | ✓ | ✓ | ✓ | ||||||
| org.projects.update | ✓ | ✓ | ✓ | ||||||
| org.projects.delete | ✓ | ✓ | |||||||
| Org: Infrastructure | |||||||||
| org.servers.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.servers.create | ✓ | ✓ | |||||||
| org.servers.update | ✓ | ✓ | |||||||
| org.servers.delete | ✓ | ✓ | |||||||
| Org: Storage | |||||||||
| org.storage.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.storage.create | ✓ | ✓ | |||||||
| org.storage.update | ✓ | ✓ | |||||||
| org.storage.delete | ✓ | ✓ | |||||||
| Org: Backups | |||||||||
| org.backups.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.backups.create | ✓ | ✓ | ✓ | ||||||
| org.backups.restore | ✓ | ✓ | |||||||
| org.backups.download | ✓ | ✓ | ✓ | ||||||
| org.backups.delete | ✓ | ✓ | |||||||
| Org: Integrations | |||||||||
| org.git.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.git.manage | ✓ | ✓ | |||||||
| org.addon_repos.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.addon_repos.manage | ✓ | ✓ | |||||||
| Org: Billing | |||||||||
| org.billing.view | ✓ | ✓ | |||||||
| org.billing.manage | ✓ | ||||||||
| Org: Settings | |||||||||
| org.settings.view | ✓ | ✓ | ✓ | ✓ | |||||
| org.settings.update | ✓ | ✓ | |||||||
| Org: Audit | |||||||||
| org.audit.view | ✓ | ✓ | ✓ | ||||||
| Org: DNS | |||||||||
| org.dns.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.dns.manage | ✓ | ✓ | |||||||
| Org: Domains | |||||||||
| org.domains.list | ✓ | ✓ | ✓ | ✓ | |||||
| org.domains.create | ✓ | ✓ | |||||||
| org.domains.delete | ✓ | ✓ | |||||||
| Org: Security | |||||||||
| org.roles.view | ✓ | ✓ | ✓ | ✓ | |||||
| org.roles.manage | ✓ | ✓ | |||||||
| Project: General | |||||||||
| project.view | ✓ | ✓ | ✓ | ||||||
| project.settings.update | ✓ | ✓ | |||||||
| Project: Environments | |||||||||
| project.environments.list | ✓ | ✓ | ✓ | ||||||
| project.environments.create | ✓ | ✓ | |||||||
| project.environments.delete | ✓ | ||||||||
| project.environments.deploy | ✓ | ✓ | |||||||
| project.environments.restart | ✓ | ✓ | |||||||
| project.environments.stop | ✓ | ||||||||
| project.environments.shell | ✓ | ||||||||
| project.environments.logs | ✓ | ✓ | ✓ | ||||||
| project.environments.config | ✓ | ✓ | |||||||
| Project: Backups | |||||||||
| project.backups.list | ✓ | ✓ | ✓ | ||||||
| project.backups.create | ✓ | ✓ | |||||||
| project.backups.restore | ✓ | ||||||||
| project.backups.download | ✓ | ✓ | |||||||
| project.backups.delete | ✓ | ||||||||
| Project: Domains | |||||||||
| project.domains.list | ✓ | ✓ | ✓ | ||||||
| project.domains.create | ✓ | ✓ | |||||||
| project.domains.delete | ✓ | ||||||||
| Project: Team | |||||||||
| project.members.manage | ✓ | ||||||||
| Project: Integrations | |||||||||
| project.repos.manage | ✓ | ✓ |
Custom Roles
Organizations can create custom roles with specific permission combinations.
Creating Custom Roles
API Endpoint: POST /api/v1/permissions/admin/roles
{
"name": "DevOps Engineer",
"scope": "organization",
"description": "Can manage infrastructure and deployments",
"color": "cyan",
"icon": "Rocket",
"organization_id": "org-123",
"permission_ids": [
"perm-1", "perm-2", "perm-3"
]
}Cloning Roles
Copy permissions from an existing role:
API Endpoint: POST /api/v1/permissions/admin/roles/{role_id}/clone
{
"name": "Custom Developer",
"description": "Developer with extra permissions",
"organization_id": "org-123"
}Use Cases
- Billing Admin: org.billing.* + org.settings.view
- Security Auditor: org.audit.view + org.members.list + org.roles.view
- Backup Manager: org.backups.* + org.storage.list
- DevOps Engineer: org.servers.* + project.environments.* + project.backups.*
- Support Engineer: Project Viewer + project.environments.logs + project.backups.download
Permission Checking (Backend)
@require_permission Decorator
Protect routes with permission checks:
from core.permissions import require_permission
@router.post("/members/invite")
@require_permission("org.members.invite")
async def invite_member(
db: DBSession,
current_user: CurrentUser,
organization_id: UUID,
):
# User must have org.members.invite permission
# organization_id from route parameters is used for context
...Custom Parameter Names
@require_permission(
"project.environments.deploy",
org_id_param="organization_id",
project_id_param="project_id"
)
async def deploy_environment(
db: DBSession,
current_user: CurrentUser,
organization_id: UUID,
project_id: UUID,
):
...Multiple Permissions
Require ANY permission:
from core.permissions import require_any_permission
@require_any_permission(
"org.members.invite",
"org.members.manage",
org_id_param="organization_id"
)
async def manage_member(...):
# User needs at least one of these permissions
...Require ALL permissions:
from core.permissions import require_all_permissions
@require_all_permissions(
"org.projects.create",
"org.billing.view",
org_id_param="organization_id"
)
async def create_paid_project(...):
# User needs both permissions
...Inline Permission Checks
from core.permissions import check_permission
async def some_endpoint(
db: DBSession,
current_user: CurrentUser,
organization_id: UUID,
):
# Check permission programmatically
if await check_permission(
db,
current_user,
"org.members.invite",
organization_id=organization_id
):
# User can invite members
send_invitation()
else:
# User cannot invite members
return {"message": "You need invite permission"}Batch Permission Checks
Check multiple permissions efficiently:
from core.permissions import check_permissions
async def some_endpoint(
db: DBSession,
current_user: CurrentUser,
organization_id: UUID,
):
# Check multiple permissions in one call
results = await check_permissions(
db,
current_user,
[
"org.projects.create",
"org.projects.update",
"org.projects.delete"
],
organization_id=organization_id
)
# results = {
# "org.projects.create": True,
# "org.projects.update": True,
# "org.projects.delete": False
# }
can_create = results["org.projects.create"]
can_delete = results["org.projects.delete"]Organization Context Validation
Always validate organization ownership:
from core.permissions import require_permission
@router.delete("/projects/{project_id}")
@require_permission("org.projects.delete")
async def delete_project(
db: DBSession,
current_user: CurrentUser,
project_id: UUID,
organization_id: UUID,
):
# Permission check passed, but verify project belongs to org
project = await db.get(Project, project_id)
if project.organization_id != organization_id:
raise HTTPException(403, "Cannot delete project from another org")
# Safe to delete
await delete_project_service(db, project)Permission Checking (Frontend)
useAbilities Hook
Fetch user permissions for the current context:
import { useAbilities } from '@/hooks/useAbilities';
function MembersPage({ organizationId }: { organizationId: string }) {
const { can, canAny, canAll, isLoading, abilities } = useAbilities({
organizationId
});
if (isLoading) return <Loading />;
return (
<div>
{can('members', 'invite') && (
<InviteButton />
)}
{can('members', 'remove') && (
<RemoveMemberButton />
)}
{canAny(['org.members.invite', 'org.members.manage']) && (
<MembersPanel />
)}
{canAll(['org.projects.create', 'org.billing.view']) && (
<CreatePaidProjectButton />
)}
</div>
);
}AbilityGate Component
Declaratively control UI visibility:
import { AbilityGate } from '@/components/permissions/AbilityGate';
function ProjectSettings({ organizationId }: { organizationId: string }) {
return (
<div>
{/* Check single permission string */}
<AbilityGate permission="org.projects.update" organizationId={organizationId}>
<EditProjectButton />
</AbilityGate>
{/* Check resource + action */}
<AbilityGate resource="projects" action="delete" organizationId={organizationId}>
<DeleteProjectButton />
</AbilityGate>
{/* Check any of multiple permissions */}
<AbilityGate
permissions={["org.projects.update", "org.projects.delete"]}
organizationId={organizationId}
>
<ProjectActionsMenu />
</AbilityGate>
{/* Require all permissions */}
<AbilityGate
permissions={["org.projects.create", "org.billing.view"]}
requireAll
organizationId={organizationId}
>
<CreatePaidProjectButton />
</AbilityGate>
{/* With fallback content */}
<AbilityGate
permission="org.members.invite"
organizationId={organizationId}
fallback={<p>You need permission to invite members</p>}
>
<InviteMemberButton />
</AbilityGate>
</div>
);
}Always Pass organizationId
CRITICAL: Always provide organizationId for org-level permissions:
// ✅ CORRECT - organizationId provided
const { can } = useAbilities({ organizationId });
// ❌ WRONG - Missing organizationId for org permissions
const { can } = useAbilities(); // Will check portal scope!usePermission Hook
Simpler hook for single permission checks:
import { usePermission } from '@/hooks/useAbilities';
function InviteButton({ organizationId }: { organizationId: string }) {
const canInvite = usePermission('members', 'invite', { organizationId });
if (!canInvite) return null;
return <button onClick={handleInvite}>Invite Member</button>;
}Defense-in-Depth
Always combine frontend and backend checks:
function DeleteProjectButton({ projectId, organizationId }) {
const { can } = useAbilities({ organizationId });
const handleDelete = async () => {
// Frontend check (UI protection)
if (!can('projects', 'delete')) {
toast.error('Permission denied');
return;
}
try {
// Backend validates permission again (security)
await apiClient.delete(`/projects/${projectId}`, {
params: { organization_id: organizationId }
});
toast.success('Project deleted');
} catch (error) {
if (error.status === 403) {
toast.error('Permission denied by server');
}
}
};
// Gate prevents button from rendering
return (
<AbilityGate permission="org.projects.delete" organizationId={organizationId}>
<button onClick={handleDelete}>Delete Project</button>
</AbilityGate>
);
}Permission Caching
Redis Cache Architecture
┌─────────────────────────────────────────────────┐
│ User Request │
│ GET /api/v1/permissions/me/abilities │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Permission Service │
│ 1. Check Redis cache │
│ Key: perm:abilities:{user_id}:{org_id} │
└─────────────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
Cache HIT Cache MISS
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ Return Cached │ │ 1. Resolve role │
│ Abilities │ │ 2. Get permissions │
│ (age < 5min) │ │ 3. Apply overrides │
└──────────────────┘ │ 4. Cache result │
│ 5. Return abilities │
└──────────────────────┘Cache Keys
Format: perm:abilities:{user_id}:{org_id}:{project_id}
Examples:
- Portal scope:
perm:abilities:user-123 - Org scope:
perm:abilities:user-123:org:org-456 - Project scope:
perm:abilities:user-123:org:org-456:proj:proj-789
Cache TTL
- Duration: 5 minutes (300 seconds)
- Storage: Redis
- Format: JSON serialized abilities
Cache Invalidation
Automatic invalidation occurs on:
- Role assigned to user → Invalidate user's cache
- Role unassigned from user → Invalidate user's cache
- Permission added/removed from role → Invalidate all users with that role
- User override created/deleted → Invalidate user's cache
- Member role changed → Invalidate user's cache (org membership)
SSE Events for Cache Invalidation
Backend broadcasts events to notify clients:
# User-specific permission change
await broadcast_to_organization(
org_id=organization_id,
event_type="permissions_changed",
data={
"user_id": str(user_id),
"message": "Your permissions have been updated"
}
)
# Role-wide permission change
await broadcast_to_organization(
org_id=organization_id,
event_type="role_permissions_changed",
data={
"role_id": str(role_id),
"role_name": role.name,
"message": f"Permissions for role '{role.name}' have been updated"
}
)Frontend automatically refreshes on events:
// useAbilities hook subscribes to SSE events
useSSEEvent('permissions_changed', handlePermissionsChanged);
useSSEEvent('role_permissions_changed', handleRolePermissionsChanged);
const handlePermissionsChanged = useCallback(() => {
console.log('[useAbilities] Permissions changed, refreshing abilities');
refresh(); // Re-fetch from API
}, [refresh]);Performance Benefits
- First request: ~50-100ms (database queries)
- Cached requests: ~2-5ms (Redis lookup)
- Cache hit rate: ~95% in production
- Reduced database load: 95% reduction in permission queries
User Permission Overrides
Temporary or permanent exceptions to role-based permissions.
Grant vs Deny
- GRANT: Give user a permission their role doesn't have
- DENY: Remove a permission their role would normally have
Creating Overrides
API Endpoint: POST /api/v1/permissions/admin/users/{user_id}/overrides
{
"permission_id": "perm-123",
"grant_type": "grant",
"organization_id": "org-456",
"expires_at": "2025-01-01T00:00:00Z",
"reason": "Temporary elevated access for migration project"
}Validation Rules
- Self-granting prevented: Cannot grant permissions to yourself
- Actor must have permission: You can only grant permissions you possess
- Reason required: All overrides require audit justification
- Scope consistency: Permission scope must match context (org/project)
- Expiration optional: Overrides can be permanent or temporary
Use Cases
Temporary Elevated Access (GRANT):
{
"user_id": "user-123",
"permission_id": "org.servers.delete",
"grant_type": "grant",
"expires_at": "2025-01-15T23:59:59Z",
"reason": "Server migration cleanup - expires after project completion"
}Security Restriction (DENY):
{
"user_id": "user-456",
"permission_id": "project.environments.shell",
"grant_type": "deny",
"reason": "Security incident investigation - shell access revoked pending review"
}Temporary Role Elevation:
{
"user_id": "user-789",
"permission_id": "org.billing.manage",
"grant_type": "grant",
"expires_at": "2024-12-31T23:59:59Z",
"reason": "Year-end billing reconciliation"
}Expiration Handling
- Expired overrides are automatically excluded from permission resolution
- Backend filters:
expires_at IS NULL OR expires_at > NOW() - No cron job needed - evaluated at permission check time
- Expired overrides remain in database for audit trail
Listing User Overrides
API Endpoint: GET /api/v1/permissions/admin/users/{user_id}/overrides
GET /api/v1/permissions/admin/users/user-123/overrides?include_expired=falseResponse:
[
{
"id": "override-1",
"user_id": "user-123",
"permission_code": "org.servers.delete",
"permission_display_name": "Remove Servers",
"grant_type": "grant",
"expires_at": "2025-01-15T23:59:59Z",
"reason": "Server migration cleanup",
"is_active": true,
"is_expired": false,
"created_at": "2024-12-01T10:00:00Z"
}
]Permission Changes
Granting Permissions to Roles
Toggle Single Permission:
POST /api/v1/permissions/admin/matrix/toggle{
"role_id": "role-123",
"permission_id": "perm-456",
"granted": true,
"organization_id": "org-789"
}Bulk Update:
PUT /api/v1/permissions/admin/matrix/bulk{
"role_id": "role-123",
"grant_permission_ids": ["perm-1", "perm-2", "perm-3"],
"revoke_permission_ids": ["perm-4", "perm-5"],
"organization_id": "org-789"
}Security Validation
All permission modifications validate:
- Actor authorization: User must have
org.roles.managepermission - Cross-org protection: Cannot modify roles from other organizations
- System role protection: Cannot modify system roles (Owner, Admin, etc.)
- Audit logging: All changes logged with actor, timestamp, old/new values
- Cache invalidation: Affected users notified via SSE
Permission Change Events
Backend logs all changes to permission_change_logs table:
await service._log_change(
change_type=PermissionChangeType.PERMISSION_GRANTED,
target_type="role_permission",
target_id=role_id,
actor_id=current_user.id,
organization_id=organization_id,
new_value={"permission_code": permission.code}
)Revoking Permissions
Same endpoints as granting, set granted: false or include in revoke_permission_ids.
Viewing User Abilities
Get Current User Abilities
API Endpoint: GET /api/v1/permissions/me/abilities
GET /api/v1/permissions/me/abilities?organization_id=org-123Response:
{
"abilities": {
"members": ["list", "invite"],
"projects": ["list", "create", "update"],
"servers": ["list"]
},
"scope": "organization",
"organization_id": "org-123",
"project_id": null,
"cached_at": "2024-12-11T10:00:00Z"
}Force Refresh Abilities
Bypass cache and recalculate:
API Endpoint: POST /api/v1/permissions/me/abilities/refresh
POST /api/v1/permissions/me/abilities/refresh?organization_id=org-123Check Single Permission
API Endpoint: POST /api/v1/permissions/check
POST /api/v1/permissions/check?permission_code=org.members.invite&organization_id=org-123Response:
{
"allowed": true,
"permission_code": "org.members.invite",
"source": "role",
"reason": null
}View Other User's Permissions (Admin)
API Endpoint: GET /api/v1/permissions/admin/users/{user_id}/permissions
GET /api/v1/permissions/admin/users/user-123/permissions?organization_id=org-456Response:
{
"user_id": "user-123",
"role_id": "role-admin",
"role_name": "Admin",
"scope": "organization",
"abilities": {
"members": ["list", "invite", "remove", "roles_update"],
"projects": ["list", "create", "update", "delete"],
"servers": ["list", "create", "update", "delete"]
},
"overrides": [
{
"id": "override-1",
"permission_code": "org.billing.manage",
"grant_type": "grant",
"reason": "Temporary billing access for invoice review",
"expires_at": "2025-01-01T00:00:00Z"
}
]
}Permission Audit
Complete audit trail for all permission-related changes.
Audit Log Table
permission_change_logs tracks:
- Actor: Who made the change (user_id)
- Timestamp: When the change occurred
- Change Type: role_created, permission_granted, override_created, etc.
- Target: What was changed (role_id, override_id, etc.)
- Old/New Values: JSON snapshot of changes
- Organization Context: Which organization (if applicable)
- Extra Metadata: IP address, user agent, etc.
Change Types
| Type | Description |
|---|---|
role_created | New role created |
role_updated | Role metadata updated |
role_deleted | Role deleted |
permission_granted | Permission added to role |
permission_revoked | Permission removed from role |
override_created | User override created |
override_updated | User override modified |
override_deleted | User override deleted |
role_assigned | Role assigned to user |
role_unassigned | Role removed from user |
bulk_update | Bulk permission changes |
Viewing Audit Logs
API Endpoint: GET /api/v1/permissions/admin/audit
GET /api/v1/permissions/admin/audit?organization_id=org-123&limit=50Response:
[
{
"id": "log-1",
"timestamp": "2024-12-11T14:30:00Z",
"actor_id": "user-admin",
"actor_name": "Admin User",
"actor_email": "admin@example.com",
"change_type": "permission_granted",
"target_type": "role_permission",
"target_id": "role-123",
"organization_id": "org-456",
"old_value": null,
"new_value": {
"permission_code": "org.servers.delete"
},
"metadata": {
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0..."
}
}
]Filter Options
organization_id: Filter by organizationactor_id: Filter by user who made changeschange_type: Filter by change typestart_date/end_date: Date rangelimit/offset: Pagination
Audit Use Cases
- Compliance: Track who has access to what and when
- Security Incidents: Investigate permission escalations
- Role Review: Periodic audits of role assignments
- User Offboarding: Verify access revocation
- Change Tracking: Monitor role evolution over time
Security Best Practices
Backend Security Checklist
1. Every Route Must Have Permission Protection
# ✅ CORRECT - Protected route
@router.post("/items")
@require_permission("org.items.create", org_id_param="organization_id")
async def create_item(...):
...
# ❌ WRONG - Unprotected route (SECURITY VULNERABILITY)
@router.post("/items")
async def create_item(...):
...2. Always Validate Organization Context
# ✅ CORRECT - Cross-org protection
if item.organization_id != organization_id:
raise HTTPException(403, "Cannot access items from other organizations")
# ❌ WRONG - No org check (allows cross-org attacks)
item = await db.get(Item, item_id)
return item3. Never Trust User Input for Authorization
# ✅ CORRECT - Verify from database
member = await db.execute(
select(Member).where(
Member.user_id == user.id,
Member.org_id == org_id
)
)
if not member:
raise HTTPException(403, "Not a member of this organization")
# ❌ WRONG - Trust user-provided role
if request.role == "admin": # User can lie!
perform_admin_action()4. Use Defense-in-Depth for Dangerous Operations
# ✅ CORRECT - Multiple checks
@require_permission("org.environments.delete")
async def delete_environment(env_id: UUID, org_id: UUID, user: User):
env = await db.get(Environment, env_id)
# Check 1: Verify org ownership
if env.organization_id != org_id:
raise HTTPException(403, "Environment not in your organization")
# Check 2: Production protection
if env.is_production:
if not await check_permission(db, user, "org.production.delete", org_id):
raise HTTPException(403, "Cannot delete production without special permission")
# Proceed with deletion
...5. Webhook Endpoints Must Validate Signatures
from core.security import verify_webhook_signature
@router.post("/webhooks/netdata/{vm_id}")
async def netdata_webhook(vm_id: UUID, request: Request):
signature = request.headers.get("X-Webhook-Signature")
body = await request.body()
if not verify_webhook_signature(body, signature, vm.netdata_webhook_secret):
raise HTTPException(401, "Invalid webhook signature")
# Process webhook
...6. Log All Security-Sensitive Operations
logger.info(
f"User {user.id} performed {action}",
extra={
"user_id": str(user.id),
"action": action,
"resource_id": str(resource_id),
"organization_id": str(org_id),
}
)Frontend Security Checklist
1. Always Wrap Action Buttons with AbilityGate
// ✅ CORRECT - Protected UI
<AbilityGate permission="org.backups.delete" organizationId={orgId}>
<Button onClick={onDelete}>Delete Backup</Button>
</AbilityGate>
// ❌ WRONG - Unprotected (shows to all users)
<Button onClick={onDelete}>Delete Backup</Button>2. Add Defense-in-Depth in Handlers
// ✅ CORRECT - Check permission before action
const handleDelete = async () => {
if (!can("backups", "delete")) {
setError("Permission denied");
return;
}
await deleteBackup(id);
};3. Protect Entire Pages/Routes
// ✅ CORRECT - Page-level protection
const { canAny, isLoading } = useAbilities({ organizationId });
useEffect(() => {
if (!isLoading && !canAny(["portal.admin"])) {
router.push("/dashboard");
}
}, [canAny, isLoading]);4. Never Show Sensitive Data Without Permission Check
// ✅ CORRECT - Conditional rendering
{can("settings", "view") && <SettingsPanel />}
// ❌ WRONG - Shows to everyone
<SettingsPanel />Security Testing
Run security tests before deployment:
# Backend security tests
cd backend
pytest tests/security/test_rbac_security.py -v
# Frontend security tests
cd frontend
npm test -- --run src/__tests__/security/Common Vulnerabilities to Avoid
| Vulnerability | Prevention |
|---|---|
| IDOR (Insecure Direct Object Reference) | Always verify object belongs to user's org |
| Privilege Escalation | Use @require_permission decorator on all routes |
| Cross-Org Data Leak | Add organization_id filter to all queries |
| Missing Auth on Webhooks | Use HMAC-SHA256 signature validation |
| UI Without Backend Check | AbilityGate + handler check + backend decorator |
| Self-Granting Permissions | Validate actor has permission before granting |
| Cache Bypass | Invalidate cache on all permission changes |
Migration from Old System
OLD System: MemberRole Enum
class MemberRole(str, enum.Enum):
OWNER = "owner"
ADMIN = "admin"
DEVELOPER = "developer" # Renamed from MEMBER
VIEWER = "viewer"Simple enum-based roles with hardcoded permissions in code.
NEW System: Permission Matrix
Granular permissions with:
- 55+ individual permissions
- 9 system roles
- Custom roles support
- User-specific overrides
- Audit trail
Coexistence Period
Both systems operate simultaneously:
- OLD routes: Still use
MemberRolechecks - NEW routes: Use
@require_permissiondecorator - Gradual migration: Routes migrated one-by-one
- No breaking changes: Existing functionality preserved
Migration Strategy
# OLD: Hardcoded role check
if membership.role in (MemberRole.OWNER, MemberRole.ADMIN):
# Can manage members
...
# NEW: Permission check
@require_permission("org.members.manage")
async def manage_members(...):
# Permission system handles authorization
...Migration Checklist
For each route being migrated:
- ✅ Identify required permission(s)
- ✅ Add
@require_permissiondecorator - ✅ Remove old
MemberRolechecks - ✅ Update frontend to use
useAbilitieshook - ✅ Replace hardcoded conditionals with
AbilityGate - ✅ Test with different roles
- ✅ Verify cache invalidation
- ✅ Update documentation
API Reference
User Abilities
Get Current User Abilities
GET /api/v1/permissions/me/abilitiesQuery Parameters:
organization_id(UUID, optional): Organization contextproject_id(UUID, optional): Project context
Response: UserAbilities
Refresh User Abilities
POST /api/v1/permissions/me/abilities/refreshQuery Parameters:
organization_id(UUID, optional): Organization contextproject_id(UUID, optional): Project context
Response: UserAbilities
Check Permission
POST /api/v1/permissions/checkQuery Parameters:
permission_code(string): Permission to checkorganization_id(UUID, optional): Organization contextproject_id(UUID, optional): Project context
Response: PermissionCheckResult
Admin: Permissions
List All Permissions
GET /api/v1/permissions/admin/permissionsQuery Parameters:
scope(PermissionScope, optional): Filter by scopecategory(string, optional): Filter by category
Response: PermissionResponse[]
Requires: Portal admin access
List Permission Categories
GET /api/v1/permissions/admin/permissions/categoriesQuery Parameters:
scope(PermissionScope, optional): Filter by scope
Response: Category list with counts
Requires: Portal admin access
Admin: Roles
List Roles
GET /api/v1/permissions/admin/rolesQuery Parameters:
scope(PermissionScope, optional): Filter by scopeorganization_id(UUID, optional): Include org's custom roles
Response: RoleResponse[]
Requires: Portal admin access
Create Role
POST /api/v1/permissions/admin/rolesRequest Body: RoleCreate
Response: RoleResponse
Requires: Portal admin access
Get Role Details
GET /api/v1/permissions/admin/roles/{role_id}Response: RoleWithPermissions
Requires: Portal admin access
Update Role
PUT /api/v1/permissions/admin/roles/{role_id}Request Body: RoleUpdate
Response: RoleResponse
Requires: Portal admin access
Note: Cannot modify system roles
Delete Role
DELETE /api/v1/permissions/admin/roles/{role_id}Response: 204 No Content
Requires: Portal admin access
Note: Cannot delete system roles
Clone Role
POST /api/v1/permissions/admin/roles/{role_id}/cloneRequest Body: RoleClone
Response: RoleResponse
Requires: Portal admin access
Admin: Permission Matrix
Get Permission Matrix
GET /api/v1/permissions/admin/matrixQuery Parameters:
scope(PermissionScope, required): Scope to vieworganization_id(UUID, optional): Organization context
Response: PermissionMatrixResponse
Requires: Portal admin access
Toggle Permission
POST /api/v1/permissions/admin/matrix/toggleRequest Body: RolePermissionToggle
Response: { success: true, granted: boolean }
Requires: Portal admin access
Bulk Update Permissions
PUT /api/v1/permissions/admin/matrix/bulkRequest Body: RolePermissionBulkUpdate
Response: { success: true, granted: number, revoked: number }
Requires: Portal admin access
Admin: User Overrides
Get User Effective Permissions
GET /api/v1/permissions/admin/users/{user_id}/permissionsQuery Parameters:
organization_id(UUID, optional): Organization contextproject_id(UUID, optional): Project context
Response: Effective permissions with overrides
Requires: Portal admin access
List User Overrides
GET /api/v1/permissions/admin/users/{user_id}/overridesQuery Parameters:
organization_id(UUID, optional): Organization contextinclude_expired(boolean, default: false): Include expired overrides
Response: UserOverrideResponse[]
Requires: Portal admin access
Create User Override
POST /api/v1/permissions/admin/users/{user_id}/overridesRequest Body: UserOverrideCreate
Response: UserOverrideResponse
Requires: Portal admin access
Validation:
- Actor must have the permission being granted
- Cannot grant permissions to yourself
- Reason required
Delete User Override
DELETE /api/v1/permissions/admin/users/{user_id}/overrides/{override_id}Response: 204 No Content
Requires: Portal admin access
Admin: Role Assignments
Assign Role to User
POST /api/v1/permissions/admin/users/{user_id}/rolesRequest Body: AssignRoleRequest
Response: UserRoleResponse
Requires: Portal admin access
Unassign Role from User
DELETE /api/v1/permissions/admin/users/{user_id}/roles/{role_id}Query Parameters:
organization_id(UUID, optional): Organization contextproject_id(UUID, optional): Project context
Response: 204 No Content
Requires: Portal admin access
List User's Role Assignments
GET /api/v1/permissions/admin/users/{user_id}/rolesQuery Parameters:
organization_id(UUID, optional): Filter by organization
Response: UserRoleResponse[]
Requires: Portal admin access
List Users Assigned to Role
GET /api/v1/permissions/admin/roles/{role_id}/usersQuery Parameters:
organization_id(UUID, optional): Filter by organization
Response: UserRoleResponse[]
Requires: Portal admin access
Admin: Audit Logs
List Permission Audit Logs
GET /api/v1/permissions/admin/auditQuery Parameters:
organization_id(UUID, optional): Filter by organizationactor_id(UUID, optional): Filter by actorstart_date(datetime, optional): Filter by start dateend_date(datetime, optional): Filter by end datelimit(int, default: 50, max: 200): Number of recordsoffset(int, default: 0): Offset for pagination
Response: PermissionChangeLogResponse[]
Requires: Portal admin access
Troubleshooting
Permission Denied Errors
Symptom: 403 Forbidden responses
Diagnosis:
- Check user's abilities:
GET /permissions/me/abilities?organization_id=X - Verify permission code matches route decorator
- Ensure
organizationIdpassed to frontend hooks - Check role assignment in database
- Look for DENY overrides
Solutions:
- Grant missing permission to role
- Create user override (GRANT)
- Verify organization membership
- Check cache hasn't expired mid-request
Cache Not Updating
Symptom: Permission changes not reflected immediately
Diagnosis:
- Check Redis connection:
redis-cli PING - Verify SSE events broadcasted: Check backend logs
- Confirm frontend subscribed to events: Check browser console
- Check cache key format matches
Solutions:
- Force refresh:
POST /permissions/me/abilities/refresh - Clear Redis cache:
redis-cli FLUSHDB(development only!) - Restart Redis:
docker-compose restart redis - Verify SSE connection: Check Network tab for
text/event-stream
Missing organizationId Context
Symptom: Portal-level permissions returned instead of org-level
Diagnosis:
// ❌ WRONG - Missing organizationId
const { can } = useAbilities();
// ✅ CORRECT
const { can } = useAbilities({ organizationId });Solution: Always pass organizationId to useAbilities() for org-scoped permissions.
Cross-Org Permission Leaks
Symptom: User can access resources from other organizations
Diagnosis:
- Check backend validates
organization_idon all queries - Verify permission decorator has
org_id_paramspecified - Look for missing
WHERE organization_id = ?clauses
Solutions:
# Add organization filter to all queries
stmt = select(Resource).where(
Resource.organization_id == organization_id
)
# Verify resource ownership before operations
if resource.organization_id != organization_id:
raise HTTPException(403, "Cross-org access denied")Portal Admin Bypass Not Working
Symptom: Portal admin getting permission denied
Diagnosis:
- Check user's
portal_rolefield in database - Verify
PortalRole.ADMINenum value - Look for custom permission checks that skip admin bypass
Solutions:
- Update user:
UPDATE users SET portal_role = 'admin' WHERE id = ? - Check decorator uses standard
require_permission - Verify
check_permission()function handles admin bypass
Summary
The OEC.SH Permission Matrix provides enterprise-grade access control with:
✅ 55+ granular permissions across portal, organization, and project scopes ✅ 9 pre-configured system roles covering all common use cases ✅ Custom roles for organization-specific needs ✅ User overrides for temporary elevated/restricted access ✅ Redis caching with 5-minute TTL for optimal performance ✅ SSE events for real-time permission invalidation ✅ Complete audit trail for compliance and security ✅ Defense-in-depth with backend + frontend validation ✅ Migration-friendly coexistence with legacy role system
This comprehensive RBAC system enables least-privilege access control, separation of duties, and flexible permission management across the entire platform.
Related Documentation: