Every action that changes something in WordPress should first ask: is this user allowed to do this? WordPress answers that question with capabilities. Getting the check right is the most important security habit in WordPress development, and the next three lessons build on it.
Roles and capabilities
A capability is a named permission, such as edit_posts, publish_posts or manage_options. A role is a named set of capabilities. Each user has one or more roles, and gets every capability those roles contain.
WordPress ships with these roles:
| Role | Typical capabilities |
|---|---|
| Administrator | Everything on a single site, including manage_options, activate_plugins and edit_users |
| Editor | Publish and manage all posts and pages, including other people's: edit_others_posts, publish_pages |
| Author | Publish and manage their own posts: publish_posts, upload_files |
| Contributor | Write and edit their own posts, but not publish them: edit_posts |
| Subscriber | Read content and manage their own profile: read |
On Multisite there is also the super admin, who can manage the whole network. The Multisite lesson covers that.
Roles and their capabilities are stored in the database, in an option named {prefix}user_roles (for example wp_user_roles). That detail matters later: changes to roles persist, so you make them once, not on every page load.
You can inspect them with WP-CLI:
wp role list
wp cap list editor
Check capabilities, not roles
The function for permission checks is current_user_can():
<?php
if ( current_user_can( 'manage_options' ) ) {
// Show or save site settings.
}
Ask about the capability the action needs, never the role. Compare these:
<?php
// Wrong: breaks for custom roles and sites that change the defaults.
if ( in_array( 'administrator', wp_get_current_user()->roles, true ) ) {
// ...
}
// Right: asks the question you actually care about.
if ( current_user_can( 'manage_options' ) ) {
// ...
}
A site might give a "Site Manager" role manage_options without making it an administrator, or remove a capability from editors. Capability checks respect all of that; role checks ignore it.
is_user_logged_in() is not a permission check. On many sites anyone can register as a subscriber. Use it only when "any logged-in user" is genuinely the rule, and even then check a capability such as read.
To check another user, use user_can( $user, 'edit_posts' ) with a user ID or WP_User object.
Meta capabilities: permission on a specific object
edit_posts answers "can this user edit posts in general?". It does not answer "can this user edit this post?". An author can edit their own posts but not an editor's.
For that WordPress has meta capabilities. You pass the object ID as an extra argument:
<?php
if ( current_user_can( 'edit_post', $post_id ) ) {
// Safe to show the edit form for this post.
}
if ( current_user_can( 'delete_post', $post_id ) ) {
// Safe to delete it.
}
Behind the scenes, map_meta_cap() turns the meta capability into the primitive capabilities the user actually needs, based on the post's author, status and type. For a published post written by someone else, edit_post maps to edit_others_posts and edit_published_posts. For your own draft it maps to edit_posts.
| Meta capability | Use it when |
|---|---|
edit_post, delete_post, read_post | Acting on one post of any type |
edit_user, delete_user | Acting on one user |
edit_term, delete_term, assign_term | Acting on one term |
edit_comment | Acting on one comment |
Always use the meta capability with an ID when the action targets one object. It is the check that prevents one user from editing another user's content by changing an ID in a URL or request.
Checks belong where the action happens
Hiding a button is not security. A request can be sent without ever seeing your interface, so the check must run in the code that performs the action.
<?php
add_action( 'admin_post_myplugin_archive_event', 'myplugin_handle_archive_event' );
function myplugin_handle_archive_event(): void {
$event_id = isset( $_POST['event_id'] ) ? absint( wp_unslash( $_POST['event_id'] ) ) : 0;
if ( ! $event_id || ! current_user_can( 'edit_post', $event_id ) ) {
wp_die(
esc_html__( 'You are not allowed to archive this event.', 'myplugin' ),
'',
array( 'response' => 403 )
);
}
check_admin_referer( 'myplugin_archive_event_' . $event_id );
wp_update_post(
array(
'ID' => $event_id,
'post_status' => 'draft',
)
);
wp_safe_redirect( admin_url( 'edit.php?post_type=myplugin_event' ) );
exit;
}
check_admin_referer() verifies a nonce, which the nonces lesson explains. The pattern is always the same: capability check and nonce check before any change.
Many APIs take a capability argument and run the check for you. add_menu_page() and add_options_page() hide the page and block access unless the user has the given capability. Use the same capability in the code that saves the page's form.
Custom capabilities
When your plugin adds a feature that does not map neatly onto core capabilities, give it its own. Suppose only some staff should manage event bookings. Create a myplugin_manage_bookings capability and check it:
<?php
if ( current_user_can( 'myplugin_manage_bookings' ) ) {
// Show the bookings screen.
}
Capabilities are stored in the database with the roles, so add them once, on plugin activation, not on every request:
<?php
/**
* Plugin Name: My Plugin Bookings
* Text Domain: myplugin
*/
defined( 'ABSPATH' ) || exit;
register_activation_hook( __FILE__, 'myplugin_activate' );
function myplugin_activate(): void {
$admin = get_role( 'administrator' );
if ( $admin ) {
$admin->add_cap( 'myplugin_manage_bookings' );
}
add_role(
'myplugin_booking_manager',
'Booking manager',
array(
'read' => true,
'myplugin_manage_bookings' => true,
)
);
}
add_role() does nothing if the role already exists, so running activation twice is safe.
Clean up when the plugin is deleted, in uninstall.php:
<?php
defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
remove_role( 'myplugin_booking_manager' );
$admin = get_role( 'administrator' );
if ( $admin ) {
$admin->remove_cap( 'myplugin_manage_bookings' );
}
Do not remove capabilities on deactivation. Deactivating a plugin to debug a problem should not strip permissions that a site owner set up.
Capabilities for a custom post type
By default a custom post type uses the post capabilities: anyone who can edit posts can edit events. To control events separately, give the post type its own capability names and turn on meta capability mapping:
<?php
register_post_type(
'myplugin_event',
array(
'label' => __( 'Events', 'myplugin' ),
'public' => true,
'show_in_rest' => true,
'capability_type' => array( 'myplugin_event', 'myplugin_events' ),
'map_meta_cap' => true,
)
);
WordPress now uses capabilities such as edit_myplugin_events, edit_others_myplugin_events, publish_myplugin_events and delete_myplugin_events, and maps edit_post on an event to them. No role has these capabilities yet, so even administrators cannot see the Events menu until you add them on activation:
<?php
function myplugin_grant_event_caps(): void {
$caps = array(
'edit_myplugin_events',
'edit_others_myplugin_events',
'edit_published_myplugin_events',
'edit_private_myplugin_events',
'publish_myplugin_events',
'read_private_myplugin_events',
'delete_myplugin_events',
'delete_others_myplugin_events',
'delete_published_myplugin_events',
'delete_private_myplugin_events',
);
foreach ( array( 'administrator', 'editor' ) as $role_name ) {
$role = get_role( $role_name );
if ( ! $role ) {
continue;
}
foreach ( $caps as $cap ) {
$role->add_cap( $cap );
}
}
}
Call myplugin_grant_event_caps() from your activation function. Your code keeps checking current_user_can( 'edit_post', $event_id ); the mapping takes care of the rest.
Changing capabilities at runtime
Sometimes a rule depends on data rather than on the role, such as "users may edit an event only until it starts". The map_meta_cap filter lets you change what a meta capability requires for a specific object. The user_has_cap filter adjusts the capabilities a user has during a check. Both are powerful and easy to get wrong, so prefer stored capabilities and reach for these filters only for rules that genuinely depend on data.
Summary
- Roles are sets of capabilities; check capabilities with
current_user_can(), never role names. - Use meta capabilities such as
edit_postwith an object ID whenever an action targets one object. - Put the check in the code that performs the action, together with a nonce check; hidden buttons are not security.
- Add custom capabilities and roles once, on activation, and remove them on uninstall.
- Give custom post types their own capabilities with
capability_typeandmap_meta_cap, then grant them to roles.