Skip to content

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 id and displayName (plus description, 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:

ParameterDescription
policyFilter by policy UUID
moduleFilter by module ID
actionFilter by action ID
limitPage size (default: 100)
offsetPage 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"
}
FieldTypeRequiredDescription
policyUUIDYesPolicy to grant permission to
modulestringYesRegistered module ID
actionstringYesAction within the module, or * for all
collection_scopestringNo__global__ (default) or collection name

Validation:

  • Module must be registered in appModuleRegistry
  • Action must exist in the module (unless *)
  • collection_scope only allowed if action has collectionScopeSupport = 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:

  1. Fetches all policies linked to the user via roles → odp_access
  2. Unions all odp_app_permissions records across those policies
  3. Returns a flat EffectiveAppPermission[] — each row carries module, action, collectionScope, grantedByPolicy, and grantedByPolicyName (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:

ParameterDescription
user_idFilter by user ID
moduleFilter by module
actionFilter by action
resultpass or fail
fromStart datetime (ISO 8601)
toEnd datetime (ISO 8601)
limitPage size
offsetPage 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

ColumnTypeDescription
idUUID PK
policyUUID FK → odp_policiesPolicy granting permission (CASCADE delete)
moduletextModule identifier
actiontextAction identifier (or *)
collection_scopetext__global__ or collection name
created_attimestamp
created_byUUID FK → odp_usersAdmin who created the permission

Unique constraint: (policy, module, action, collection_scope)

Indexes:

  • idx_app_perm_policy on policy
  • idx_app_perm_module_action on (module, action)

odp_app_access_logs

ColumnTypeDescription
idUUID PK
user_idtextUser making the request
initiated_bytextActual user (differs if impersonating)
moduletextModule being accessed
actiontextAction being performed
collection_scopetextCollection scope (if applicable)
resulttextpass or fail
policy_idtextPolicy that granted access (on pass)
fail_reasontextReason for failure (on fail)
iptextClient IP
user_agenttextBrowser/client
created_attimestamp

Indexes:

  • idx_app_log_user on user_id
  • idx_app_log_created on created_at
  • idx_app_log_module_action on (module, action)
  • idx_app_log_result on result

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.

ODP Internal API Documentation