Skip to main content

Permission Model

Overview​

Open ACE combines role-based access control (RBAC) with multi-tenant boundary enforcement:

  • 6 roles in the UserRole universe (app/models/user.py:14-22): user, readonly, manager, tenant_admin, platform_admin, plus the legacy admin
  • 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.

RoleTenant scopePersistable?Description
platform_adminAll tenantsYesPlatform administrator; can manage every tenant (whether legacy admin also counts depends on strict mode, see below)
tenant_adminOwn tenant onlyYesTenant administrator; must have a non-null tenant_id (DB constraint chk_2332_tenant_admin_requires_tenant)
admin (legacy)System-wide for compatibilityNo — excluded from PERSISTABLE_ROLES; existing accounts keep working, create/update/restore reject itLegacy super-admin kept for backward compatibility (Issues #2286, #2332); migrate these accounts to platform_admin
manager—YesTeam manager: 11 permissions (view/export across messages, analysis, users, quota, audit, content filter)
user—YesRegular user: 4 permissions (view_dashboard, view_messages, view_analysis, view_quota)
readonly—YesDashboard 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):

Permissionadminmanageruserreadonly
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:

DecoratorUsesOne-line responsibility
@admin_required91Accepts 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_required51Requires a valid login; optional ownership='session' or ownership='machine' check that admin roles bypass
@platform_admin_required25Requires a platform admin per strict mode; logs legacy admin acceptance as deprecated
@same_tenant_or_platform_admin8Tenant-scoped resource access: a tenant admin for the resource's own tenant, or any platform admin (tenant keyword governance endpoints)
@machine_access_required9Remote-machine access: system admin or a user assigned to the machine
@same_tenant_user_required8Stacked 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_required4Remote-machine administration: system admin or the machine's designated admin
@api_key_admin_required4API-key management authorization (Issue #2327): resolves the authorized target tenant and sets g.actor_scope; see the dedicated section below
@any_admin_required4Any admin role, for non-cross-tenant operations (user management within own tenant, tenant-neutral config, audit access)
@tenant_member_required1Admin 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_admin and legacy admin pass is_platform_admin_role()
  • Strict on: only platform_admin passes; legacy admin accounts stop being platform admins and get 403 on platform-admin endpoints

Operational rules:

  1. 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 from create_app() so the mode is frozen and logged before the first request
  2. Migrate every remaining role='admin' account to platform_admin before enabling strict mode; warn_if_strict_mode_locks_out_legacy_admins() shouts at startup if strict mode would lock out legacy accounts
  3. 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​

  1. Client sends POST /api/auth/login with {username, password}
  2. Server validates credentials — bcrypt with 12 rounds (hash_password() in app/routes/auth.py:72, same in admin.py / tenant.py)
  3. Creates a session record with expiry
  4. Sets the session_token cookie (HttpOnly, Secure over HTTPS)
  5. 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 or before_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 (/health is 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; QuotaEnforcementScheduler runs 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); omitting tenant_id uses 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.