Features
Organization
Permissions & RBAC

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.action naming 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

  1. Scope: portal, org, or project
  2. Resource: Entity being accessed (e.g., members, projects, backups)
  3. 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 backups

Action Types

ActionDescriptionTypical Use
listView a list of resourcesGET /api/resources
viewView details of a resourceGET /api/resources/:id
createCreate new resourcesPOST /api/resources
updateModify existing resourcesPUT/PATCH /api/resources/:id
deleteDelete resourcesDELETE /api/resources/:id
manageFull CRUD accessAdmin operations
deployDeployment operationsEnvironment deployments
restartRestart operationsContainer restarts
stopStop operationsContainer stops
shellShell/terminal accessContainer exec
logsView logsLog streaming
configConfiguration accessEdit configs
restoreRestore operationsBackup restores
downloadDownload accessBackup downloads
roles_updateRole assignmentChange member roles

Complete Permission Matrix

Portal Permissions (15)

Platform-wide permissions for system administrators.

Users (5 permissions)

CodeDisplay NameDescriptionDangerous
portal.users.listView UsersView list of all platform usersNo
portal.users.createCreate UsersCreate new platform usersNo
portal.users.updateEdit UsersModify user details and rolesNo
portal.users.statusToggle User StatusActivate or deactivate user accountsYes
portal.users.deleteDelete UsersPermanently delete users from the platformYes

Organizations (3 permissions)

CodeDisplay NameDescriptionDangerous
portal.organizations.listView OrganizationsView all organizations on the platformNo
portal.organizations.createCreate OrganizationsCreate new organizationsNo
portal.organizations.deleteDelete OrganizationsPermanently delete organizations and all their dataYes

Infrastructure (1 permission)

CodeDisplay NameDescriptionDangerous
portal.servers.manageManage ServersFull server management at platform levelYes

Security (1 permission)

CodeDisplay NameDescriptionDangerous
portal.permissions.manageManage PermissionsConfigure roles and permissions system-wideYes

Settings (2 permissions)

CodeDisplay NameDescriptionDangerous
portal.settings.viewView Platform SettingsView platform configurationNo
portal.settings.updateEdit Platform SettingsModify platform configurationYes

Platform (1 permission)

CodeDisplay NameDescriptionDangerous
portal.odoo_versions.manageManage Odoo VersionsAdd, update, or remove supported Odoo versionsNo

Integrations (2 permissions)

CodeDisplay NameDescriptionDangerous
portal.git_connections.manageManage Platform Git ConnectionsConfigure platform-level Git integrationsNo
portal.addon_repos.manageManage Platform Addon ReposConfigure platform-level addon repositoriesNo

Organization Permissions (37)

Organization-scoped permissions for tenant management.

Team (4 permissions)

CodeDisplay NameDescriptionDangerous
org.members.listView MembersView organization team membersNo
org.members.inviteInvite MembersInvite new members to the organizationNo
org.members.removeRemove MembersRemove members from the organizationYes
org.members.roles.updateChange Member RolesModify roles assigned to membersYes

Projects (4 permissions)

CodeDisplay NameDescriptionDangerous
org.projects.listView ProjectsView organization projectsNo
org.projects.createCreate ProjectsCreate new projects in the organizationNo
org.projects.updateEdit ProjectsModify project settingsNo
org.projects.deleteDelete ProjectsPermanently delete projects and all environmentsYes

Infrastructure (4 permissions)

CodeDisplay NameDescriptionDangerous
org.servers.listView ServersView organization serversNo
org.servers.createAdd ServersAdd new servers to the organizationNo
org.servers.updateEdit ServersModify server configurationNo
org.servers.deleteRemove ServersRemove servers from the organizationYes

Storage (4 permissions)

CodeDisplay NameDescriptionDangerous
org.storage.listView Storage ConfigsView backup storage configurationsNo
org.storage.createCreate Storage ConfigAdd new storage providers for backupsNo
org.storage.updateEdit Storage ConfigModify storage provider settingsNo
org.storage.deleteDelete Storage ConfigRemove storage configurationsYes

Backups (5 permissions)

CodeDisplay NameDescriptionDangerous
org.backups.listView BackupsView all organization backupsNo
org.backups.createCreate BackupsInitiate backup operationsNo
org.backups.restoreRestore BackupsRestore data from backupsYes
org.backups.downloadDownload BackupsDownload backup files from storageNo
org.backups.deleteDelete BackupsPermanently delete backup filesYes

Integrations (4 permissions)

CodeDisplay NameDescriptionDangerous
org.git.listView Git ConnectionsView organization Git integrationsNo
org.git.manageManage Git ConnectionsConfigure organization Git integrationsNo
org.addon_repos.listView Addon ReposView organization addon repositoriesNo
org.addon_repos.manageManage Addon ReposConfigure organization addon repositoriesNo

Billing (2 permissions)

CodeDisplay NameDescriptionDangerous
org.billing.viewView BillingView invoices and subscription detailsNo
org.billing.manageManage BillingUpdate payment methods and subscriptionsYes

Settings (2 permissions)

CodeDisplay NameDescriptionDangerous
org.settings.viewView SettingsView organization settingsNo
org.settings.updateEdit SettingsModify organization settingsNo

Audit (1 permission)

CodeDisplay NameDescriptionDangerous
org.audit.viewView Audit LogsView organization activity logsNo

DNS (2 permissions)

CodeDisplay NameDescriptionDangerous
org.dns.listView DNS ConfigsView DNS provider configurationsNo
org.dns.manageManage DNS ConfigsConfigure DNS providersNo

Domains (3 permissions)

CodeDisplay NameDescriptionDangerous
org.domains.listView DomainsView organization domainsNo
org.domains.createAdd DomainsAdd new domains to organizationNo
org.domains.deleteRemove DomainsRemove domains from organizationYes

Security (2 permissions)

CodeDisplay NameDescriptionDangerous
org.roles.viewView RolesView organization roles and permissionsNo
org.roles.manageManage RolesCreate, edit, and delete organization rolesYes

Project Permissions (22)

Project-scoped permissions for application management.

General (2 permissions)

CodeDisplay NameDescriptionDangerous
project.viewView ProjectAccess project overview and detailsNo
project.settings.updateEdit Project SettingsModify project configurationNo

Environments (9 permissions)

CodeDisplay NameDescriptionDangerous
project.environments.listView EnvironmentsView project environmentsNo
project.environments.createCreate EnvironmentsCreate new environmentsNo
project.environments.deleteDelete EnvironmentsPermanently delete environmentsYes
project.environments.deployDeployDeploy code changes to environmentsNo
project.environments.restartRestartRestart environment containersNo
project.environments.stopStopStop running environmentsYes
project.environments.shellShell AccessAccess environment command lineYes
project.environments.logsView LogsView environment logsNo
project.environments.configEdit ConfigModify environment configurationNo

Backups (5 permissions)

CodeDisplay NameDescriptionDangerous
project.backups.listView BackupsView project backupsNo
project.backups.createCreate BackupsCreate new backupsNo
project.backups.restoreRestore BackupsRestore from backupsYes
project.backups.downloadDownload BackupsDownload backup filesNo
project.backups.deleteDelete BackupsDelete backup filesYes

Domains (3 permissions)

CodeDisplay NameDescriptionDangerous
project.domains.listView DomainsView project domainsNo
project.domains.createAdd DomainsAdd new domains to environmentsNo
project.domains.deleteRemove DomainsRemove domains from environmentsYes

Team (1 permission)

CodeDisplay NameDescriptionDangerous
project.members.manageManage Project MembersAdd/remove project-level membersYes

Integrations (1 permission)

CodeDisplay NameDescriptionDangerous
project.repos.manageManage RepositoriesConfigure project repositoriesNo

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

PermissionPortal AdminPortal ManagerOwnerAdminDeveloperViewerProject AdminProject DeveloperProject 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:

  1. Role assigned to user → Invalidate user's cache
  2. Role unassigned from user → Invalidate user's cache
  3. Permission added/removed from role → Invalidate all users with that role
  4. User override created/deleted → Invalidate user's cache
  5. 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

  1. Self-granting prevented: Cannot grant permissions to yourself
  2. Actor must have permission: You can only grant permissions you possess
  3. Reason required: All overrides require audit justification
  4. Scope consistency: Permission scope must match context (org/project)
  5. 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=false

Response:

[
  {
    "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:

  1. Actor authorization: User must have org.roles.manage permission
  2. Cross-org protection: Cannot modify roles from other organizations
  3. System role protection: Cannot modify system roles (Owner, Admin, etc.)
  4. Audit logging: All changes logged with actor, timestamp, old/new values
  5. 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-123

Response:

{
  "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-123

Check Single Permission

API Endpoint: POST /api/v1/permissions/check

POST /api/v1/permissions/check?permission_code=org.members.invite&organization_id=org-123

Response:

{
  "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-456

Response:

{
  "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

TypeDescription
role_createdNew role created
role_updatedRole metadata updated
role_deletedRole deleted
permission_grantedPermission added to role
permission_revokedPermission removed from role
override_createdUser override created
override_updatedUser override modified
override_deletedUser override deleted
role_assignedRole assigned to user
role_unassignedRole removed from user
bulk_updateBulk permission changes

Viewing Audit Logs

API Endpoint: GET /api/v1/permissions/admin/audit

GET /api/v1/permissions/admin/audit?organization_id=org-123&limit=50

Response:

[
  {
    "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 organization
  • actor_id: Filter by user who made changes
  • change_type: Filter by change type
  • start_date / end_date: Date range
  • limit / offset: Pagination

Audit Use Cases

  1. Compliance: Track who has access to what and when
  2. Security Incidents: Investigate permission escalations
  3. Role Review: Periodic audits of role assignments
  4. User Offboarding: Verify access revocation
  5. 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 item

3. 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

VulnerabilityPrevention
IDOR (Insecure Direct Object Reference)Always verify object belongs to user's org
Privilege EscalationUse @require_permission decorator on all routes
Cross-Org Data LeakAdd organization_id filter to all queries
Missing Auth on WebhooksUse HMAC-SHA256 signature validation
UI Without Backend CheckAbilityGate + handler check + backend decorator
Self-Granting PermissionsValidate actor has permission before granting
Cache BypassInvalidate 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:

  1. OLD routes: Still use MemberRole checks
  2. NEW routes: Use @require_permission decorator
  3. Gradual migration: Routes migrated one-by-one
  4. 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:

  1. ✅ Identify required permission(s)
  2. ✅ Add @require_permission decorator
  3. ✅ Remove old MemberRole checks
  4. ✅ Update frontend to use useAbilities hook
  5. ✅ Replace hardcoded conditionals with AbilityGate
  6. ✅ Test with different roles
  7. ✅ Verify cache invalidation
  8. ✅ Update documentation

API Reference

User Abilities

Get Current User Abilities

GET /api/v1/permissions/me/abilities

Query Parameters:

  • organization_id (UUID, optional): Organization context
  • project_id (UUID, optional): Project context

Response: UserAbilities

Refresh User Abilities

POST /api/v1/permissions/me/abilities/refresh

Query Parameters:

  • organization_id (UUID, optional): Organization context
  • project_id (UUID, optional): Project context

Response: UserAbilities

Check Permission

POST /api/v1/permissions/check

Query Parameters:

  • permission_code (string): Permission to check
  • organization_id (UUID, optional): Organization context
  • project_id (UUID, optional): Project context

Response: PermissionCheckResult


Admin: Permissions

List All Permissions

GET /api/v1/permissions/admin/permissions

Query Parameters:

  • scope (PermissionScope, optional): Filter by scope
  • category (string, optional): Filter by category

Response: PermissionResponse[]

Requires: Portal admin access

List Permission Categories

GET /api/v1/permissions/admin/permissions/categories

Query 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/roles

Query Parameters:

  • scope (PermissionScope, optional): Filter by scope
  • organization_id (UUID, optional): Include org's custom roles

Response: RoleResponse[]

Requires: Portal admin access

Create Role

POST /api/v1/permissions/admin/roles

Request 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}/clone

Request Body: RoleClone

Response: RoleResponse

Requires: Portal admin access


Admin: Permission Matrix

Get Permission Matrix

GET /api/v1/permissions/admin/matrix

Query Parameters:

  • scope (PermissionScope, required): Scope to view
  • organization_id (UUID, optional): Organization context

Response: PermissionMatrixResponse

Requires: Portal admin access

Toggle Permission

POST /api/v1/permissions/admin/matrix/toggle

Request Body: RolePermissionToggle

Response: { success: true, granted: boolean }

Requires: Portal admin access

Bulk Update Permissions

PUT /api/v1/permissions/admin/matrix/bulk

Request 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}/permissions

Query Parameters:

  • organization_id (UUID, optional): Organization context
  • project_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}/overrides

Query Parameters:

  • organization_id (UUID, optional): Organization context
  • include_expired (boolean, default: false): Include expired overrides

Response: UserOverrideResponse[]

Requires: Portal admin access

Create User Override

POST /api/v1/permissions/admin/users/{user_id}/overrides

Request 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}/roles

Request 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 context
  • project_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}/roles

Query 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}/users

Query 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/audit

Query Parameters:

  • organization_id (UUID, optional): Filter by organization
  • actor_id (UUID, optional): Filter by actor
  • start_date (datetime, optional): Filter by start date
  • end_date (datetime, optional): Filter by end date
  • limit (int, default: 50, max: 200): Number of records
  • offset (int, default: 0): Offset for pagination

Response: PermissionChangeLogResponse[]

Requires: Portal admin access


Troubleshooting

Permission Denied Errors

Symptom: 403 Forbidden responses

Diagnosis:

  1. Check user's abilities: GET /permissions/me/abilities?organization_id=X
  2. Verify permission code matches route decorator
  3. Ensure organizationId passed to frontend hooks
  4. Check role assignment in database
  5. 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:

  1. Check Redis connection: redis-cli PING
  2. Verify SSE events broadcasted: Check backend logs
  3. Confirm frontend subscribed to events: Check browser console
  4. 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:

  1. Check backend validates organization_id on all queries
  2. Verify permission decorator has org_id_param specified
  3. 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:

  1. Check user's portal_role field in database
  2. Verify PortalRole.ADMIN enum value
  3. 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: