Overview
Approval Gates add a human-in-the-loop checkpoint before the most destructive actions in WordPress can execute. Instead of outright blocking capabilities like policies do, approvals let users request permission and wait for an administrator to approve or deny the action.
Approval Gates are a Cloud-only feature. The module checks for a valid adminlocks_cloud_key in the options table before initializing. If no Cloud key exists, the approval system is entirely inactive and imposes zero overhead.
Gated Capabilities
AdminLocks gates 9 WordPress capabilities that represent the most risky administrative actions. These capabilities cover the three areas where a single wrong click can take a site down: plugins, themes, and user management.
| Category | Capability | What It Controls |
|---|---|---|
| Plugins | install_plugins |
Adding new plugins from the repository or uploads |
activate_plugins |
Turning on an installed plugin | |
delete_plugins |
Permanently removing a plugin from the server | |
| Themes | switch_themes |
Changing the active theme |
install_themes |
Adding new themes from the repository or uploads | |
delete_themes |
Permanently removing a theme from the server | |
| Users | promote_users |
Changing a user's role (e.g., editor to administrator) |
create_users |
Adding new WordPress user accounts | |
delete_users |
Removing user accounts and their content |
How It Works
The approval system intercepts capabilities at the WordPress user_has_cap filter (priority 20, which runs after the policy filter at priority 10). When a non-bypass user attempts a gated action, AdminLocks checks whether an approved record exists for that user and capability.
The interception flow
- WordPress core calls
current_user_can('activate_plugins')(or any gated capability). - The
user_has_capfilter fires. AdminLocks checks if the user is a super admin or hasadminlocks_bypass— if so, the capability passes through unchanged. - For non-bypass users, AdminLocks queries the
adminlocks_approvalstable for a record matching the user ID and capability withstatus = 'approved'andreviewed_atwithin the last hour. - If an approved record exists, the capability is allowed. If not, the capability is set to
false, effectively preventing the action.
// The core interception logic (simplified)
foreach ($caps as $cap) {
if (in_array($cap, $this->gated_actions, true)) {
if (!$this->has_approved_action($user->ID, $cap)) {
$allcaps[$cap] = false;
}
}
}
The approval check has a 1-hour execution window. Once an action is approved, the user has 60 minutes to perform it. After that, the approved record is no longer valid for the user_has_cap check and they would need to request approval again.
Admin Notice
When pending approvals exist, AdminLocks displays a warning notice at the top of every admin page for users with manage_options. The notice shows the count of pending requests and links to the approvals management page at admin.php?page=adminlocks-approvals.
Approval Lifecycle
Every approval request follows a defined lifecycle with clear state transitions.
pending ──┬──> approved ──┬──> executed
│ └──> expired (if not used within 1 hour)
├──> denied
└──> expired (if past expires_at)
Submitting a request
Requests are submitted programmatically via the Approvals::submit() method. Each request includes the action type (capability name), contextual data (what plugin, what theme, etc.), and the requesting user's ID.
// Submit an approval request
$approvals = new \AdminLocks\Modules\Approvals();
$id = $approvals->submit('activate_plugins', [
'plugin' => 'woocommerce/woocommerce.php',
'reason' => 'Client needs e-commerce functionality'
]);
// The submit method:
// 1. Inserts a record with status = 'pending'
// 2. Calculates expires_at based on adminlocks_approval_expire_hours
// 3. Logs the request to the audit log
// 4. Fires the adminlocks_approval_submitted action hook
Reviewing a request
Administrators review requests via the REST API or the wp-admin approvals page. The review method accepts the approval ID, a status (approved or denied), and an optional note.
// Approve a request
$approvals->review($id, 'approved', 'Looks good, go ahead.');
// Deny a request
$approvals->review($id, 'denied', 'Not needed at this time.');
Each review action is logged to the audit log and fires the corresponding hook: adminlocks_approval_approved or adminlocks_approval_denied.
Execution tracking
After an approval is granted and the user performs the action, AdminLocks marks the approval as executed. This tracking is handled automatically through WordPress action hooks:
activated_plugin— consumesactivate_pluginsapprovalsswitch_theme— consumesswitch_themesapprovalsuser_register— consumescreate_usersapprovals
The consumption process finds the most recent approved record for the user and action type (within the 1-hour window) and updates its status to executed.
Auto-Expiration
Approval requests have a configurable expiration time. By default, pending requests expire after 72 hours. This is controlled by the adminlocks_approval_expire_hours option.
// Set expiration to 24 hours
update_option('adminlocks_approval_expire_hours', 24);
// Disable expiration (requests stay pending forever)
update_option('adminlocks_approval_expire_hours', 0);
An hourly cron job (adminlocks_approval_cleanup) runs the expiration check. It updates all pending records where expires_at is in the past to status = 'expired'.
There are two distinct time windows: the pending expiration (default 72 hours, configurable) controls how long a request waits for review. The execution window (fixed at 1 hour) controls how long after approval the user can perform the action.
Cloud Integration
When connected to AdminLocks Cloud, approval requests are synced to the Cloud dashboard where they can be reviewed from a centralized interface. This enables several workflow enhancements:
- Remote approval — approve or deny requests from the Cloud dashboard without logging into the WordPress site.
- Email notifications — get notified when new approval requests are submitted across any connected site.
- Webhook integration — use the Cloud webhook system to approve or deny requests programmatically from external tools (Slack, Teams, etc.).
- Cross-site visibility — see all pending approvals across all your sites in a single view.
Extensibility hooks
Three action hooks allow you to extend the approval workflow with custom logic:
// Fired when a new approval is submitted
add_action('adminlocks_approval_submitted', function($id, $action_type, $action_data) {
// Send a Slack notification, create a Jira ticket, etc.
}, 10, 3);
// Fired when an approval is approved
add_action('adminlocks_approval_approved', function($id) {
// Notify the requesting user, update external systems, etc.
});
// Fired when an approval is denied
add_action('adminlocks_approval_denied', function($id) {
// Notify the requesting user with the denial reason
});
REST API
The approval system exposes two REST endpoints. Both require the manage_options capability.
| Method | Endpoint | Description |
|---|---|---|
GET |
/adminlocks/v1/approvals |
List all pending approval requests |
POST |
/adminlocks/v1/approvals/{id}/review |
Approve or deny a specific request |
Review request body
// POST /wp-json/adminlocks/v1/approvals/42/review
{
"status": "approved", // or "denied"
"note": "Approved for production deployment."
}
// Response
{ "ok": true }
List pending approvals response
// GET /wp-json/adminlocks/v1/approvals
[
{
"id": 42,
"action_type": "activate_plugins",
"action_data": "{\"plugin\":\"woocommerce/woocommerce.php\"}",
"requested_by": 3,
"requested_email": "client@example.com",
"status": "pending",
"expires_at": "2026-03-08 14:22:10",
"reviewed_by": null,
"review_note": null,
"reviewed_at": null,
"created_at": "2026-03-05 14:22:10"
}
]