Skip to content

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:

Example: Granting permissions in Drupal

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. Where X 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

  1. A string, this is a single permission to be checked
  2. 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
  3. 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 permission Drupal: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.

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