Drupal 10 : Fonctionnement des permissions

Découvrez comment fonctionne le socle du système de droits de Drupal.

Cet article a été initialement rédigé pour Drupal 8 mais son contenu est toujours d'actualité pour Drupal 9 et Drupal 10.

N'hésitez pas à nous contacter ou à vous inscrire à notre formation «Drupal pour les développeurs et développeuses» pour en savoir plus !

Comme dans Drupal 7, le contrôle d’accès de base des nouvelles versions de Drupal repose sur les permissions rattachées aux rôles des utilisateurs. Même si le concept de base n’a pas changé, la façon dont il est implémenté a lui été repensé pour être plus cohérent avec le reste.

Définition des permissions

Ainsi, les rôles sont désormais des entités de configuration identifiées par un nom machine et disposant d’un label traduisible. Elles portent également un booléen is_admin qui remplace la variable user_admin_role de Drupal 7 et permet de définir un ou plusieurs rôles disposant de toutes les permissions. Enfin, contrairement à Drupal 7 dans lequel c’étaient les permissions qui portaient la liste des rôles rattachés (du moins lors d’un export via Features), chaque rôle contient désormais une liste des permissions qui lui sont associées.

# user.role.authenticated.yml
uuid: 31ee396d-455b-48c6-ae00-aad6f105fe13
langcode: en
status: true
dependencies: {  }
id: authenticated
label: 'Authenticated user'
weight: 1
is_admin: false
permissions:
 - 'access content'
 - 'use text format basic_html'
 - 'access comments'
 - 'post comments'
 - 'skip comment approval'
 - 'access site-wide contact form'
 - 'access shortcuts'

Les permissions quant à elles restent aussi simples qu’avant. Au lieu d’utiliser un hook_permission() pour les définir, on utilise un fichier *.permissions.yml comme suit :

# my_things.permissions.yml
administer my things:
  title: 'Administer my things'
  description: 'Create, update and delete all my things.'
  restrict access: true
access my things:
  title: 'Access my things'
  description: 'View all my things.'

Depuis Drupal 9.3.0, les permissions peuvent désormais déclarer des dépendances (EN). Cela permet notamment de nettoyer plus facilement les rôles lorsque la configuration évolue dans le temps.

Étant donné que la déclaration des permissions ne se fait plus en PHP, il n’est plus aussi évident de les définir de façon dynamique. Il est toutefois possible de spécifier une ou plusieurs fonctions de callback à appeler pour y parvenir. Cela se fait via la clef permission_callbacks du fichier *.permissions.yml :

# my_things.permissions.yml
permission_callbacks:
  - \Drupal\my_things\MyThingPermissions::dynamicPermissions
# MyThingPermissions.php
namespace Drupal\my_things;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\my_things\Entity\ThingType;
/**
 * Provides dynamic permissions for my things.
 */
class MyThingsPermissions {

  use StringTranslationTrait;

  /**
   * Returns an array of permissions.
   *
   * @return array
   *   The permissions.
   *   @see \Drupal\user\PermissionHandlerInterface::getPermissions()
   */
  public function dynamicPermissions() {
    $perms = array();
    // Generate permissions for all things types.
    foreach (ThingType::loadMultiple() as $type) {
      $type_id = $type->id();
      $type_params = ['%thing' => $type->label()];
      $perms += [
        "create $type_id thing" => [
          'title' => $this->t('%thing: Create new thing', $type_params),
        ],
        "edit $type_id thing" => [
          'title' => $this->t('%thing: Edit any thing', $type_params),
        ],
        "delete $type_id thing" => [
          'title' => $this->t('%thing: Delete any thing', $type_params),
        ],
      ];
    }
    return $perms;
  }
}

Pour finir il est important de noter qu’il n’est toujours pas possible actuellement d’altérer les permissions définies par le cœur ou par un autre module. La demande de la communauté n’étant pas spécialement pressante, il est peu probable que l’on voit apparaître cette fonctionnalité dans les prochains mois.

Contrôle d’accès simple

Dans Drupal 7, la façon la plus simple et rapide de tester si l’utilisateur dispose d’une permission est l’appel à la fonction user_access(). Dans Drupal 10, cette fonction a été remplacée par la méthode AccountInterface::hasPermission().

# Controller.php
// Vérification que l'utilisateur courant a une permission donnée.
\Drupal::currentUser()->hasPermission('name of permission');
// La même vérification pour un utilisateur précis.
$account = User::load('3');
$account->hasPermission('name of permission');

D’autres contrôles d’accès étaient faits de façon plus automatique comme par exemple lorsque l’on spécifiait un nom de permission dans une clef access arguments d’une entrée du hook_menu(). Dans Drupal 10, on retrouve une clef équivalente dans la partie requirements de nos routes.

Exemple :

# my_things.routing.yml
my_things.settings:
 path: '/admin/config/my_things'
 defaults:
   _form: '\Drupal\my_things\Form\SettingsForm'
 requirements:
   _permission: 'administer my things'

Nouveauté par rapport à Drupal 7 : il est désormais possible d’associer plusieurs permissions à une même route en les séparant soit par une virgule, soit par un plus (+). La virgule équivaut à un ET alors que le plus équivaut à un OU. Il n’est pas possible de combiner les virgules et les plus en utilisant cette syntaxe.

Exemples :

_permission: 'administer my things+administer content'
_permission: 'access thing TYPE,access restricted things'

Contrôle d’accès avancé

Si d’aventure vous souhaitez avoir une logique plus avancée pour gérer le contrôle d’accès, il faut utiliser la clé _custom_access au lieu de _permission, sa valeur est la référence d’une méthode à appeler intégrant la classe à laquelle elle est rattachée. À noter que le contrôleur d’accès doit retourner un objet de type AccessResultInterface.

Exemple :

# my_things.routing.yml
my_things.all:
 path: '/things/all'
 defaults:
   _controller: '\Drupal\my_things\Controller\ThingsController::all'
   _title: 'All things'
 requirements:
   _custom_access: '\Drupal\my_things\Controller\ThingsAccess::all'
# ThingsAccess.php
namespace Drupal\my_things\Controller;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
class ThingsAccess {
  /**
   * Randomly limit access to the things.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   */
  public function all(AccountInterface $account) {
    if (rand() > 0.5) {
      return AccessResult::allowed();
    }
    return AccessResult::forbidden();
  }
}

Les objets de type AccessResultInterface disposent de nombreuses méthodes statiques très utiles pour autoriser ou restreindre l’accès de façon conditionnelle tout en évitant les problèmes liés aux performances. Par exemple, vous pourriez choisir de restreindre l’accès à un contenu en fonction d’un état calculé de manière complexe et des permissions de l’utilisateur tout en garantissant que cet accès redevienne fonctionnel au moment même où l’état change ou à tout moment pour les utilisateurs autorisés.

Exemple :

# my_things.routing.yml
entity.my_things.canonical:
 path: '/things/{thing}'
 defaults:
   _controller: '\Drupal\my_things\Controller\ThingsController::view'
   _title_callback: '\Drupal\my_things\Controller\ThingsController::title'
 requirements:
   _custom_access: '\Drupal\my_things\Controller\ThingsAccess::view'
# ThingsAccess.php
namespace Drupal\my_things\Controller;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\my_things\Entity\Thing;
class ThingsAccess {
  /**
   * Limit access to the things according to their restricted state.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   * @param \Drupal\my_things\Entity\Thing $thing
   */
  public function view(AccountInterface $account, Thing $thing) {
    $is_restricted = $thing->myComplexState() === 'restricted';
    $has_permission = AccessResult::allowedIfHasPermission($account, 'access restricted   things');

    $result = AccessResult::allowedIf($is_restricted)->andIf($has_permission);
    $result->addCacheableDependency($thing);

    return $result;
  }
}

Dans cet exemple, on met le résultat en cache pour tous les utilisateurs ayant les mêmes permissions (grâce à l’appel à allowedIfHasPermission) et tant que l’entité $thing n’est pas modifiée. Ainsi, même si le calcul effectué dans $thing->myComplexState() est coûteux, il n’aura d’impact sur l’accès à ce contenu que la première fois que l’on interrogera cette méthode d’accès.

Commentaires

stève Mercredi 12 juillet 2017 - 11:55
Salut, Merci pour tes articles, ça aide vraiment pour le passage de D7 à D8 :-) J'ai une question qui a peut-être pas trop de rapport, mais je tente quand même... J'ai dans le profil de certains users un champ avec la liste des id produits que le user peut modifier. Comment je peux avoir ou non le bouton ou l'onglet d'édition sur chaque node produit et dans les vues, en fonction de si l'id produit affiché (ou à afficher dans les vues) se trouve dans la liste du user courant ? Si tu as une piste je suis preneur ;-)

Votre commentaire

Le contenu de ce champ sera maintenu privé et ne sera pas affiché publiquement.
Votre adresse servira à afficher un Gravatar et à vous notifier des réponses. Votre commentaire sera anonymisé si ce billet est dépublié pendant plus de 3 mois.
Pour lutter contre le spam notre système enregistre votre adresse IP et votre adresse e-mail si vous la partagez.
Nous vous invitons à consulter notre politique de confidentialité pour comprendre les traitements faits de ces données et comment les rectifier.

À propos de Julien

Co-fondateur - Scrum master & Expert technique

Utilisateur de Drupal depuis 2008, j’ai fait mes armes comme développeur chez Commerce Guys puis me suis mis à encadrer les nouveaux arrivants avant de donner des formations, participer aux avant ventes et accompagner les équipes au passage à Scrum.

Je suis impliqué dans la communauté française de Drupal depuis 2009, j’ai été tour à tour président puis vice-président de l’association Drupal France et francophonie entre 2011 et 2013.