Permission Model
Overview
Open ACE combines role-based access control (RBAC) with multi-tenant boundary enforcement:
- 6 roles in the
UserRoleuniverse (app/models/user.py:14-22):user,readonly,manager,tenant_admin,platform_admin, plus the legacyadmin - 19 permissions assigned through 4 built-in RBAC role definitions (
app/services/permission_service.py:72-113,DEFAULT_ROLES) - 10 authentication decorators (see the inventory below) that cover login, admin tiers, machine access, and tenant boundaries
- One strict-mode switch,
OPENACE_PLATFORM_ADMIN_STRICT_MODE, that narrows who counts as a platform admin
For the endpoint-by-endpoint view, see API_PERMISSION_MATRIX.md (same directory, generated by scripts/generate_permission_matrix.py).
Role Universe
The authoritative sources are UserRole / ADMIN_ROLES / PERSISTABLE_ROLES in app/models/user.py:14-30 and ROLE_GROUPS in app/constants/role_groups.py.
| Role | Tenant scope | Persistable? | Description |
|---|---|---|---|
platform_admin | All tenants | Yes | Platform administrator; can manage every tenant (whether legacy admin also counts depends on strict mode, see below) |
tenant_admin | Own tenant only | Yes | Tenant administrator; must have a non-null tenant_id (DB constraint chk_2332_tenant_admin_requires_tenant) |
admin (legacy) | System-wide for compatibility | No — excluded from PERSISTABLE_ROLES; existing accounts keep working, create/update/restore reject it | Legacy super-admin kept for backward compatibility (Issues #2286, #2332); migrate these accounts to platform_admin |
manager | — | Yes | Team manager: 11 permissions (view/export across messages, analysis, users, quota, audit, content filter) |
user | — | Yes | Regular user: 4 permissions (view_dashboard, view_messages, view_analysis, view_quota) |
readonly | — | Yes | Dashboard viewing only (view_dashboard) |
ADMIN_ROLES = frozenset({"admin", "platform_admin", "tenant_admin"}) is what "is an admin" means throughout the codebase (User.is_admin, ownership-check bypasses). For display statistics, roles collapse into three groups (app/constants/role_groups.py): admin (admin/platform_admin/tenant_admin), manager, and user (user/readonly).
The built-in RBAC permission matrix (19 permissions, from DEFAULT_ROLES in app/services/permission_service.py:72-113):
| Permission | admin | manager | user | readonly |
|---|---|---|---|---|
| view_dashboard | ✓ | ✓ | ✓ | ✓ |
| view_messages | ✓ | ✓ | ✓ | |
| export_messages | ✓ | ✓ | ||
| view_analysis | ✓ | ✓ | ✓ | |
| run_analysis | ✓ | ✓ | ||
| export_analysis | ✓ | ✓ | ||
| view_users | ✓ | ✓ | ||
| create_user / edit_user / delete_user | ✓ | |||
| manage_permissions | ✓ | |||
| view_quota | ✓ | ✓ | ✓ | |
| manage_quota | ✓ | |||
| view_audit_logs / export_audit_logs | ✓ | ✓ | ||
| view_content_filter | ✓ | ✓ | ||
| manage_content_filter | ✓ | |||
| admin_access / system_config | ✓ |
admin_access acts as a superuser bypass — a role holding it passes every permission check (Role.has_permission).
Note that tenant_admin / platform_admin are admin-tier roles for decorator purposes (they satisfy ADMIN_ROLES); the DEFAULT_ROLES matrix above covers the non-admin tier plus the legacy admin entry that owns all 19 permissions.
Authentication Decorators (10)
All definitions live in app/auth/decorators.py except machine_access_required / machine_admin_required (app/routes/remote.py:1059-1088) and api_key_admin_required (app/auth/decorators.py:1984). Usage counts are call sites under app/ as of this writing:
| Decorator | Uses | One-line responsibility |
|---|---|---|
@admin_required | 91 | Accepts any admin role (admin/platform_admin/tenant_admin) via session token or WebUI URL token; deprecated (Issue #2332) — migrate to any_admin_required or platform_admin_required |
@auth_required | 51 | Requires a valid login; optional ownership='session' or ownership='machine' check that admin roles bypass |
@platform_admin_required | 25 | Requires a platform admin per strict mode; logs legacy admin acceptance as deprecated |
@same_tenant_or_platform_admin | 8 | Tenant-scoped resource access: a tenant admin for the resource's own tenant, or any platform admin (tenant keyword governance endpoints) |
@machine_access_required | 9 | Remote-machine access: system admin or a user assigned to the machine |
@same_tenant_user_required | 8 | Stacked below an admin decorator; confines user-targeted admin endpoints (reset password, update, delete…) to the actor's tenant and protects platform-level accounts from tenant admins |
@machine_admin_required | 4 | Remote-machine administration: system admin or the machine's designated admin |
@api_key_admin_required | 4 | API-key management authorization (Issue #2327): resolves the authorized target tenant and sets g.actor_scope; see the dedicated section below |
@any_admin_required | 4 | Any admin role, for non-cross-tenant operations (user management within own tenant, tenant-neutral config, audit access) |
@tenant_member_required | 1 | Admin roles plus manager, for tenant-scoped alert endpoints (Issue #3082) |
Two markers are not counted as auth decorators: @public_endpoint flags intentionally unauthenticated routes for the security scanner, and @security_annotated(reason=...) flags endpoints whose ownership checks are inline rather than decorator-based.
For admin endpoints that take a tenant_id filter or act on a non-user resource, the decorators are paired with helpers in the same module: resolve_tenant_scope() / require_tenant_scope() (non-admins are tenant-scoped and denied when no tenant resolves — fail closed per Issue #1775), enforce_requested_tenant_scope(), enforce_resource_tenant_scope(), and enforce_target_user_tenant(). All fail closed on missing or inconsistent tenant ids instead of silently widening scope.
Platform Admin Strict Mode
OPENACE_PLATFORM_ADMIN_STRICT_MODE (app/auth/permissions.py:19-46) controls who counts as a platform admin:
- Strict off (default): both
platform_adminand legacyadminpassis_platform_admin_role() - Strict on: only
platform_adminpasses; legacyadminaccounts stop being platform admins and get 403 on platform-admin endpoints
Operational rules:
- The value is read once at startup and cached for the process lifetime — changing it requires a restart.
init_platform_admin_strict_mode()is called fromcreate_app()so the mode is frozen and logged before the first request - Migrate every remaining
role='admin'account toplatform_adminbefore enabling strict mode;warn_if_strict_mode_locks_out_legacy_admins()shouts at startup if strict mode would lock out legacy accounts - The flag governs checks that route through
is_platform_admin_role()/User.is_platform_admin()(platform_admin_required,same_tenant_or_platform_admin,resolve_admin_tenant_scope). Code comparing role strings directly (e.g.app/routes/mapping_rules.py) is not affected — enabling the flag narrows authorization but does not remove the legacy role
See docs/dev-notes/2332-admin-role-migration-runbook.md for the migration runbook.
Authentication
Login Flow
- Client sends
POST /api/auth/loginwith{username, password} - Server validates credentials — bcrypt with 12 rounds (
hash_password()inapp/routes/auth.py:72, same inadmin.py/tenant.py) - Creates a session record with expiry
- Sets the
session_tokencookie (HttpOnly, Secure over HTTPS) - Returns the user profile with role and
tenant_id
Rate Limiting
Failed login attempts are tracked in the login_attempts table (lockout thresholds come from security settings, default max 5 attempts). Security settings are cached for 60 seconds.
Token Extraction
Session tokens are extracted from safe sources only — cookie → Authorization: Bearer header. Query-parameter session tokens are rejected per Issue #1896. A small allowlist of path prefixes (/api/admin/, /api/remote/terminal/, /api/remote/vscode/, /api/workspace/, /api/projects) accepts special URL tokens (WebUI v1/v2, proxy, browser tokens), validated by format and always audit-logged.
Forced Password Change
If a user is marked must_change_password=true, every decorator blocks the request except the minimum endpoints: auth check, profile, change-password, logout, and password policy.
Route Protection
- Manage mode (
/manage/*) — all admin pages require an admin role; regular users and machine admins cannot reach them - API routes — most
/api/*blueprints authenticate via decorator orbefore_request; sensitive operations stack the admin-tier decorators above - Public routes:
/— SPA catch-all (serves index.html)/api/auth/login— login endpoint/api/auth/check— auth status check/livez,/readyz,/metrics,/security-status— probes and metrics (/healthis deprecated and delegates to/readyz)
Custom Permissions
Beyond the built-in roles, per-user permissions can be granted:
PermissionService.grant_permission(user_id, 'export_analysis', granted_by=admin_id)
has_perm = PermissionService.has_permission(user_id, 'export_analysis')
Custom permissions are stored in the user_permissions table and combined with the user's role permissions.
Multi-Tenant Isolation
When multi-tenant mode is enabled:
- Users are associated with a tenant via
tenant_id - Tenant quotas enforce per-tenant token and request limits;
QuotaEnforcementSchedulerruns every 60s, terminating exceeded sessions and generating alerts - Tenant-aware boundaries cover users, projects, workspace sessions, session messages, daily usage aggregates, audit logs, remote machines, machine permissions, and quotas
- Non-admin user-facing APIs apply the authenticated tenant scope to session, project, usage, and audit queries
- Platform admins intentionally retain global operational visibility for support and incident response; every cross-tenant reach is audit-logged (
ADMIN_CROSS_TENANT_ACCESS)
API Key Tenant Authorization (Issue #2327)
The tenant_id parameter on API-key management endpoints is a target selector, not an authorization credential. The route-layer guard is @api_key_admin_required, which calls resolve_authorized_target_tenant(actor, requested_tenant_id):
- tenant_admin: target tenant always comes from the auth context (
g.tenant_id); omittingtenant_iduses the actor tenant (backward compatible), any other tenant returns 403 - platform_admin / legacy admin: must explicitly pass
tenant_id; omitting it returns 400 (fail closed, no global list), cross-tenant operations are allowed and audit-logged - Ownership checks on
PUT/DELETE /api/api-keys/<key_id>return 403 (never 404) for keys outside the target tenant, so ids cannot be probed
The service/repository layers receive an immutable ActorScope (validated by @require_actor_scope()), so authorization cannot be bypassed by calling deeper layers directly. Everything fails closed: no default-tenant fallback, no silently ignored invalid tenant_id, no global listing.
Key storage, rotation, and proxy semantics are covered in KEY_MANAGEMENT.md; the full endpoint-by-endpoint permission tables are in API_PERMISSION_MATRIX.md.