Route and Hook Approval
Route and Hook Approval
All plugin routes and hooks start unapproved. This is a security gate: plugin code does not execute until an admin explicitly approves it. This page covers the approval workflow in detail.
Why Approval Exists
Plugins run arbitrary Lua code. Without an approval gate, installing a plugin directory would immediately expose new HTTP endpoints and inject code into content lifecycle events. The approval system ensures an admin reviews what a plugin does before it can affect the system.
- Unapproved routes return 404 as if they do not exist.
- Unapproved hooks are silently skipped during content operations.
There is no partial execution. A route is either fully approved and serving traffic, or completely invisible.
Approval States
Each route and hook has one of three states:
| State | Description |
|---|---|
| Unapproved | Default for new routes/hooks. No code executes. |
| Approved | Admin has explicitly approved. Route serves traffic / hook fires on events. |
| Revoked | Previously approved, then explicitly revoked. Same behavior as unapproved. |
There is no functional difference between "unapproved" and "revoked" -- both prevent execution. The distinction exists for audit clarity.
Version-Change Behavior
When a plugin's version field in plugin_info changes, all route and hook approvals for that plugin are automatically revoked. This happens during plugin loading, before any code executes.
This prevents a scenario where:
- Admin approves plugin v1.0.0 routes after reviewing the code.
- Developer updates the plugin to v2.0.0 with different behavior.
- The updated routes serve traffic without review.
After a version bump, the admin must re-approve all routes and hooks. Use --all-routes and --all-hooks flags for bulk re-approval.
Version-change revocation does not fire if only the Lua code changes without a version bump. To enforce re-approval on code changes without version bumps, use the revoke commands manually.
Approving via CLI
The CLI uses a Bearer token auto-generated at server startup (written to <config_dir>/.plugin-api-token). Pass --token <value> for CI/CD environments.
Approve Routes
# Approve all routes for a plugin
modulacms plugin approve my_plugin --all-routes
# Approve a specific route
modulacms plugin approve my_plugin --route "GET /tasks"
modulacms plugin approve my_plugin --route "POST /tasks"
modulacms plugin approve my_plugin --route "GET /tasks/{id}"
# Skip confirmation prompt (for CI/CD)
modulacms plugin approve my_plugin --all-routes --yes
Approve Hooks
# Approve all hooks for a plugin
modulacms plugin approve my_plugin --all-hooks
# Approve a specific hook (format: "event:table")
modulacms plugin approve my_plugin --hook "before_create:content_data"
modulacms plugin approve my_plugin --hook "after_update:content_data"
# Wildcard hooks use "*" as the table
modulacms plugin approve my_plugin --hook "after_delete:*"
# Skip confirmation prompt
modulacms plugin approve my_plugin --all-hooks --yes
Revoke Routes
# Revoke all routes
modulacms plugin revoke my_plugin --all-routes
# Revoke a specific route
modulacms plugin revoke my_plugin --route "GET /tasks"
Revoke Hooks
# Revoke all hooks
modulacms plugin revoke my_plugin --all-hooks
# Revoke a specific hook
modulacms plugin revoke my_plugin --hook "before_create:content_data"
Combined Approval
Approve both routes and hooks in one command:
modulacms plugin approve my_plugin --all-routes --all-hooks --yes
Approving via API
All approval endpoints require authentication with plugins:admin permission. Read endpoints require plugins:read.
List Routes
curl http://localhost:8080/api/v1/admin/plugins/routes \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Response includes each route's plugin name, method, path, public flag, and approval status.
Approve Routes
curl -X POST http://localhost:8080/api/v1/admin/plugins/routes/approve \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"routes": [
{"plugin": "task_tracker", "method": "GET", "path": "/tasks"},
{"plugin": "task_tracker", "method": "POST", "path": "/tasks"},
{"plugin": "task_tracker", "method": "GET", "path": "/tasks/{id}"},
{"plugin": "task_tracker", "method": "PUT", "path": "/tasks/{id}"},
{"plugin": "task_tracker", "method": "DELETE", "path": "/tasks/{id}"}
]
}'
Revoke Routes
curl -X POST http://localhost:8080/api/v1/admin/plugins/routes/revoke \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"routes": [
{"plugin": "task_tracker", "method": "GET", "path": "/tasks"}
]
}'
List Hooks
curl http://localhost:8080/api/v1/admin/plugins/hooks \
-H "Cookie: session=YOUR_SESSION_COOKIE"
Response includes each hook's plugin name, event, table, priority, wildcard flag, and approval status.
Approve Hooks
curl -X POST http://localhost:8080/api/v1/admin/plugins/hooks/approve \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"hooks": [
{"plugin": "task_tracker", "event": "before_create", "table": "content_data"},
{"plugin": "task_tracker", "event": "after_update", "table": "content_data"}
]
}'
Revoke Hooks
curl -X POST http://localhost:8080/api/v1/admin/plugins/hooks/revoke \
-H "Cookie: session=YOUR_SESSION_COOKIE" \
-H "Content-Type: application/json" \
-d '{
"hooks": [
{"plugin": "task_tracker", "event": "before_create", "table": "content_data"}
]
}'
Approving via TUI
The SSH TUI includes a Plugins page accessible from the homepage menu:
- Navigate to the Plugins screen from the home menu.
- Select a plugin from the list.
- The plugin detail view shows all registered routes and hooks with their approval status.
- Use the approve action to approve individual routes or hooks through a confirmation dialog.
CI/CD Patterns
For automated deployments, script the approval after deploying the plugin:
#!/usr/bin/env bash
PLUGIN_NAME="task_tracker"
TOKEN=$(cat /opt/modulacms/.plugin-api-token)
# Wait for plugin to load
sleep 2
# Approve all routes and hooks
modulacms plugin approve "$PLUGIN_NAME" \
--all-routes --all-hooks \
--yes \
--token "$TOKEN"
# Verify
modulacms plugin info "$PLUGIN_NAME" --token "$TOKEN"
For API-based CI/CD without the CLI:
#!/usr/bin/env bash
BASE="http://localhost:8080/api/v1/admin/plugins"
SESSION="YOUR_CI_SESSION_COOKIE"
# Approve routes
curl -X POST "$BASE/routes/approve" \
-H "Cookie: session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"routes": [
{"plugin": "task_tracker", "method": "GET", "path": "/tasks"},
{"plugin": "task_tracker", "method": "POST", "path": "/tasks"}
]
}'
# Approve hooks
curl -X POST "$BASE/hooks/approve" \
-H "Cookie: session=$SESSION" \
-H "Content-Type: application/json" \
-d '{
"hooks": [
{"plugin": "task_tracker", "event": "after_create", "table": "content_data"}
]
}'
Idempotent Behavior
All approval and revocation operations are idempotent:
- Approving an already-approved route or hook is a no-op.
- Revoking an already-revoked or never-approved route or hook is a no-op.
- No errors are returned for redundant operations.
This makes CI/CD scripts safe to re-run without conditional logic.