Appearance
App Permissions Module
Overview
The App Permissions system controls access to specific application features (modules) beyond standard data CRUD permissions. While odp_permissions controls which collections/fields a user can read/write, App Permissions control things like: can this user access the MCP interface? Can they view the workflow module? Can they manage notifications?
Permissions are granted per policy (which maps to roles via odp_access). Each permission record specifies:
- Which module (e.g.,
mcp,workflow,notify) - Which action (e.g.,
access,manage,view) - Optional collection scope (restrict to a specific collection)
Key Concepts
Modules
Modules are registered in appModuleRegistry. Each module defines:
- A module
idanddisplayName(plusdescription,source) - A set of allowed actions (
key,displayName,description,riskLevel) - Whether each action supports collection-level scoping (
collectionScopeSupport)
Policies
App permissions link to odp_policies, not directly to roles. A role can have multiple policies via odp_access. Effective permissions = union of all permissions across all policies linked to the user's roles.
Endpoints
All endpoints require Admin access unless noted.
GET /app-permissions/modules
List all registered app modules.
Response:
json
{
"data": [
{
"id": "mcp",
"displayName": "MCP",
"description": "Model Context Protocol access control",
"source": "built-in",
"actions": [
{ "key": "access", "displayName": "Access MCP", "description": "Connect to and use MCP tools", "riskLevel": "medium", "collectionScopeSupport": false },
{ "key": "write", "displayName": "MCP Write Operations", "description": "Use MCP tools that modify data", "riskLevel": "high", "collectionScopeSupport": false }
]
},
{
"id": "workflow",
"displayName": "Workflow",
"description": "Approval workflows and task management",
"source": "built-in",
"actions": [
{ "key": "view", "displayName": "View Workflows", "description": "View workflow definitions and instances", "riskLevel": "low", "collectionScopeSupport": false },
{ "key": "start", "displayName": "Start Workflow", "description": "Start new workflow instances", "riskLevel": "medium", "collectionScopeSupport": true },
{ "key": "participate", "displayName": "Participate in Workflow", "description": "Approve, reject, delegate, and comment on workflow steps", "riskLevel": "medium", "collectionScopeSupport": true },
{ "key": "manage", "displayName": "Manage Workflows", "description": "Cancel instances, view all tasks, and manage workflow audit logs", "riskLevel": "high", "collectionScopeSupport": false }
]
}
]
}Each module is { id, displayName, description, source, actions[] }; each action is { key, displayName, description, riskLevel, collectionScopeSupport }. Built-in modules registered in appModuleRegistry: assistant, ai, workflow, versioning, mcp, extension-settings, notify (src/permissions/app-module-registry.ts).
GET /app-permissions/modules/:moduleId
Get a specific module definition.
URL Parameters:
moduleId— Module identifier
Response: Single module object (same structure as list item).
GET /app-permissions
List all app permission records.
Query Parameters:
| Parameter | Description |
|---|---|
policy | Filter by policy UUID |
module | Filter by module ID |
action | Filter by action ID |
limit | Page size (default: 100) |
offset | Page offset (default: 0) |
Response:
json
{
"data": [
{
"id": "perm-uuid",
"policy": "policy-uuid",
"module": "mcp",
"action": "access",
"collectionScope": "__global__",
"createdAt": "2024-01-01T00:00:00.000Z",
"createdBy": "user-uuid"
}
]
}POST /app-permissions
Create a new app permission.
Request Body:
json
{
"policy": "policy-uuid",
"module": "workflow",
"action": "start",
"collection_scope": "my_collection"
}| Field | Type | Required | Description |
|---|---|---|---|
policy | UUID | Yes | Policy to grant permission to |
module | string | Yes | Registered module ID |
action | string | Yes | Action within the module, or * for all |
collection_scope | string | No | __global__ (default) or collection name |
Validation:
- Module must be registered in
appModuleRegistry - Action must exist in the module (unless
*) collection_scopeonly allowed if action hascollectionScopeSupport = true- Policy must exist in
odp_policies - Unique constraint:
(policy, module, action, collection_scope)
Response: 201 Created — returns only the new record's id (not the full record).
json
{
"data": {
"id": "new-perm-uuid"
}
}DELETE /app-permissions/:id
Delete an app permission record.
URL Parameters:
id— Permission UUID
Response: 204 No Content
Side Effect: Invalidates app permission cache for all users linked to the permission's policy.
GET /app-permissions/user/:userId/effective
Get the effective (resolved) app permissions for a specific user.
Auth required: Admin
URL Parameters:
userId— User UUID
Response: a flat array of EffectiveAppPermission — one entry per granted (module, action, collectionScope), not a nested map.
json
{
"data": [
{
"module": "mcp",
"action": "access",
"collectionScope": "__global__",
"grantedByPolicy": "policy-uuid",
"grantedByPolicyName": "MCP Users"
},
{
"module": "workflow",
"action": "manage",
"collectionScope": "__global__",
"grantedByPolicy": "policy-uuid",
"grantedByPolicyName": "Workflow Admins"
}
]
}Business Logic:
- Fetches all policies linked to the user via roles →
odp_access - Unions all
odp_app_permissionsrecords across those policies - Returns a flat
EffectiveAppPermission[]— each row carriesmodule,action,collectionScope,grantedByPolicy, andgrantedByPolicyName(src/services/app-permissions.ts:94-135)
GET /app-permissions/me/effective
Get effective app permissions for the current authenticated user.
Auth required: Yes
Response: Same structure as /user/:userId/effective.
Audit Logs
GET /app-access-logs
Query app permission access log entries.
Auth required: Admin
Query Parameters:
| Parameter | Description |
|---|---|
user_id | Filter by user ID |
module | Filter by module |
action | Filter by action |
result | pass or fail |
from | Start datetime (ISO 8601) |
to | End datetime (ISO 8601) |
limit | Page size |
offset | Page offset |
Response:
json
{
"data": [
{
"id": "log-uuid",
"user_id": "user-uuid",
"initiated_by": "user-uuid",
"module": "mcp",
"action": "access",
"collection_scope": null,
"result": "pass",
"policy_id": "policy-uuid",
"fail_reason": null,
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"created_at": "2024-01-01T12:00:00.000Z"
}
]
}Data Model
odp_app_permissions
| Column | Type | Description |
|---|---|---|
id | UUID PK | |
policy | UUID FK → odp_policies | Policy granting permission (CASCADE delete) |
module | text | Module identifier |
action | text | Action identifier (or *) |
collection_scope | text | __global__ or collection name |
created_at | timestamp | |
created_by | UUID FK → odp_users | Admin who created the permission |
Unique constraint: (policy, module, action, collection_scope)
Indexes:
idx_app_perm_policyonpolicyidx_app_perm_module_actionon(module, action)
odp_app_access_logs
| Column | Type | Description |
|---|---|---|
id | UUID PK | |
user_id | text | User making the request |
initiated_by | text | Actual user (differs if impersonating) |
module | text | Module being accessed |
action | text | Action being performed |
collection_scope | text | Collection scope (if applicable) |
result | text | pass or fail |
policy_id | text | Policy that granted access (on pass) |
fail_reason | text | Reason for failure (on fail) |
ip | text | Client IP |
user_agent | text | Browser/client |
created_at | timestamp |
Indexes:
idx_app_log_useronuser_ididx_app_log_createdoncreated_atidx_app_log_module_actionon(module, action)idx_app_log_resultonresult
Permission Validation Flow
The environment gate lives at each module's own route, not inside validateAppAccess. Every module route computes USE_APP_PERMISSIONS = process.env.ENABLE_APP_PERMISSIONS !== 'false' (e.g. src/modules/notify/routes.ts:43, src/modules/workflow/routes.ts, src/routes/versions.ts, src/modules/mcp/odp-mcp.ts). So enforcement is on by default — an unset variable enforces; only the explicit value false disables it.