Permissions Framework in CiviCRM¶
Introduction¶
CiviCRM defines a number of entities, screens, APIs, and other resources. Access to these resources is dictated by permissions. Permissions are ordinarily administered on a screen like this:
The details of the screen will vary based on the CMS and configuration. However, in all cases, there is a list of permissions (e.g. add contacts
, view all contacts
). For CiviCRM development,
our primary concerns are defining the list of available permissions and enforcing those permissions.
Examples¶
CiviCRM defines a large number of permissions (100+ and counting). However, there are a handful of permissions which are frequently referenced by developers and administrators. These examples provide a good starting-point to understand permissions generally.
administer CiviCRM
- This is a very broad permission and generally speaking is designed to grant access to any administrative parts of CiviCRM.edit all contacts
,view all contacts
- This grants access to all contacts within the database for editing purposes.access all custom data
- This grants the user access to view and edit all the custom data in the system. When viewing data screens ("Edit Contact", "Edit Contribution", etc), this will enable panels for custom data.access CiviContribute
,access CiviEvent
, etc - These permissions are for each core module e.g. CiviContribute CiviEvent. WhereX
will be the title of that module. These permissions grant access to those areas of CiviCRM. These permissions will only show up if the modules are enabled.
Conventions¶
- Lower-case is preferred.
- Proper nouns (e.g. "CiviCRM" or "CiviEvent") may be capitalized.
- Spaces separate words.
- The
:
indicates an optional namespace for foreign permissions (e.g.Drupal:administer users
) - The
@
prefix indicates an atypical, synthetic permission. - Punctuation is otherwise reserved/avoided.
Checking permissions¶
To determine if a user has a permission, CiviCRM uses an internal API, CRM_Core_Permission::check()
.
if (! CRM_Core_Permission::check('update widgets')) {
CRM_Core_Session::setStatus('', ts('Insufficient permission'), 'error');
}
Under the hood, check()
delegates to a user-management system (e.g. Drupal users/roles or WordPress users/roles).
The behavior of check()
may be modified programmatically via hook_civicrm_permission_check.
The permission that is passed to the check function can be in a few different formats
- A string, this is a single permission to be checked
- An array e.g.
['access CiviCRM', 'access AJAX API']
In this case this means "AND" because its a single layer array. So the user would have to have both permissions - A two dimension array e.g.
[['access CiviCRM', 'access AJAX API'], 'access CiviEvent']
In this example the inner Array means an OR statement i.e the user must have Either Access CiviCRM OR access AJAX API but must also have access CiviEvent permission as that is a separate value within the outer array so is treated as an AND.
Defining permissions¶
The list of CiviCRM permissions is defined in PHP. It draws upon these key sources:
- Core Permissions: Common permissions available on all CiviCRM configurations. See:
CRM_Core_Permissions::getCorePermissions()
. - Component Permissions: Each component ("CiviEvent", "CiviMail", etc) defines its own list of permissions. See:
CRM_*_Info::getPermissions()
. - Hook Permissions: Extensions may also define their own permissions. See: hook_civicrm_permission.
To inspect the full list of defined permissions (regardless of origin), use the Permission
API (v5.34+):
$ cv api4 Permission.get -T +s name +w 'name like %user%'
+------------------------------------+
| name |
+------------------------------------+
| edit user-driven message templates |
| cms:administer users |
| Drupal:administer users |
+------------------------------------+
Compare: hook_civicrm_permission
and hook_civicrm_permissionList
There are two hooks which influence the list of permissions. They are subtly different:
- hook_civicrm_permission: Define new permissions. For example, CiviVolunteer defines the new permission
register to volunteer
. This permission did not exist anywhere until CiviVolunteer defined it. - hook_civicrm_permissionList: Modify the list of permissions presented by
Permission.get
. For example, the permissionDrupal:administer users
originates in Drupal, but we may want to offer it in some CiviCRM configuration screens. It is added to the permission list.
Enforcing permissions¶
All permission enforcement boils down to CRM_Core_Permission::check(...)
. This building-block can be mixed into any kind of logic:
if (CRM_Core_Permission::check('set custom deadlines') && !empty($form['deadline'])) {
$newRecord->deadline = $form['deadline'];
}
else {
$newRecord->deadline = max(strtotime('+7 days'), Moon::getNextFullMoon());
}
Permission enforcement is usually more structured. Let's review some conventions used by important subsystems.
APIv4¶
In APIv4, each entity and action declares minimum permissions. These permissions are automatically enforced when receiving an API call via PHP or REST/AJAX.
For most existing entities in civicrm-core
, the declarations are listed in CRM_Core_Permission::getEntityActionPermissions()
. This example declares the minimal permission for Contact.create
and Contact.delete
:
$permissions['contact'] = [
'create' => ['access CiviCRM', 'add contacts'],
'delete' => ['access CiviCRM', 'delete contacts'],
];
What about new APIs? By default, new APIs (in an extension or in core) require the permission administer CiviCRM
. This can be overriden by the new entity:
namespace Civi\Api4;
class MyEntity extends \Civi\Api4\Generic\AbstractEntity {
public static function permissions() {
return [
'meta' => ['access CiviCRM'],
'default' => ['administer CiviCRM'],
];
}
}
These declarations describe a default minimum permission. There are two common variations:
-
Additional checks: Within the logic of an API method, one may enforce more nuanced permissions by consulting
CRM_Core_Permission::check(...)
. -
Toggle enforcement: When calling APIv4 via PHP, one may optionally disregard permissions. This can be useful if you are building a new wrapper API or server-side form.
// Example: Disable API permission checks (array notation) civicrm_api4('MyEntity', 'myAction', ['checkPermissions' => FALSE]); // Example: Disable API permission checks (method noation) \Civi\Api4\MyEntity::myAction()->setCheckPermissions(FALSE)->execute(); // Example: Disable API permission checks (short-hand notation) \Civi\Api4\MyEntity::myAction(0)->execute();
Sometimes extension authors want to modify the permissions of an entity that extension authors don't own e.g. Contact,Contribution either on a whole of entity basis or for specific actions. Note for an extension author's own custom entities they should implement the permissions method described abvoe. To do this you need to implement an symfony event listener to listen on the civi.api.authorize
event. It is recommended that your custom event runs prior to the stndard CiviCRM event.
An implementation may look like this note that this relies on the scan-classes mixin being enabled.
namespace Civi\Api4\Event\Subscriber;
use Civi\API\Events;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class MyCustomCheckSubscriber extends \Civi\Core\Service\AutoService implements EventSubscriberInterface {
/**
* @return array
*/
public static function getSubscribedEvents() {
return [
'civi.api.authorize' => [
['onApiAuthorize', 100],
],
];
}
/**
* @param \Civi\API\Event\AuthorizeEvent $event
* API authorization event.
*/
public function onApiAuthorize(\Civi\API\Event\AuthorizeEvent $event) {
/** @var \Civi\Api4\Generic\AbstractAction $apiRequest */
$apiRequest = $event->getApiRequest();
if ($apiRequest['version'] == 4) {
if ($apiRequest['entity'] === 'Contact' && $apiRequest['action'] === 'mycustomaction') {
if (CRM_Core_Permission::check('my custom permission')) {
$event->authorize();
$event->stopPropagation();
}
}
}
}
}
APIv3¶
In APIv3, each entity and action is declared with a minimum permission level. These permissions are automatically enforced when receiving an API call via REST/AJAX.
When called via PHP, APIv3 permissions are not enforced. PHP callers may opt-in with the check_permissions
flag.
As with APIv4, most permissions are declared in CRM_Core_Permission::getEntityActionPermissions()
. Again, this example declares the minimal permission for Contact.create
and Contact.delete
:
$permissions['contact'] = [
'create' => ['access CiviCRM', 'add contacts'],
'delete' => ['access CiviCRM', 'delete contacts'],
];
What about new APIs? Again, as with APIv4, the default requirement is administer CiviCRM
. To override this default, you must implement hook_civicrm_alterAPIPermissions.
Routing¶
Routes or pages (such as civicrm/dashboard
and civicrm/admin
) are declared in XML. You may specify the minimum permission required to open the page (e.g.
<access_arguments>administer CiviCRM</access_arguments>
). For more details, see Framework: Routing.
Navigation¶
hook_civicrm_navigationMenu allows for extension providers to define new menu items and the associated permissions to that menu item. However this does not specifically grant access to the end point just decides whether the menu item or not is visible to the user based on the permissions of that user.
HTML_QuickForm¶
Many administrative screens are based on CRM_Core_Page_Basic
and/or HTML_QuickForm
. These screens may use some combination of methods:
CRM_Core_Page_Basic::getIdAndAction()
CRM_Core_Page_Basic::checkPermission()
CRM_Core_Permission::checkActionPermission()
Cross-over naming¶
CiviCRM's permission subsystem integrates with the permission subsystems in Drupal, WordPress, etc. The integration is fairly deep -- so deep that one may even forget that they are different subsystems. However, differences do exist. Let's consider some guidelines for when these differences matter.
The differences will not matter if you consistently use the same provider to register and test permissions:
Provider | Register permissions | Test permissions |
---|---|---|
Civi | hook_civicrm_permission |
CRM_Core_Permission::check($perm) |
D7 | hook_permission |
user_access($perm) |
WP | add_cap() |
current_user_can($perm) |
The differences may matter if you have a cross-over use-case -- such a WordPress module with a guard based on a CiviCRM permission, or a CiviCRM extension with a guard based on CMS permission. For cross-over, you may need to modify the permission name. Here are a few examples:
// Test for CiviCRM's 'edit all contacts'
CRM_Core_Permission::check('edit all contacts'); // Civi native
user_access('edit all contacts'); // D7=>Civi, identical
current_user_can('edit_all_contacts'); // WP=>Civi, munged
// Test for permission to manage CMS user
user_access('administer users'); // D7 native
current_user_can('edit_users'); // WP native
CRM_Core_Permission::check('Drupal:administer users'); // Civi=>D7, prefix
CRM_Core_Permission::check('WordPress:edit_users'); // Civi=>WP, prefix
CRM_Core_Permission::check('cms:administer users'); // Civi=>{$CMS}, dynamic