access control

switch_roles.install
<?php

use Drupal\user\RoleInterface;

/**
 * Remove the old roles and replace them with the new ones.
 */
function MY_MODULE_update_9041(): void {
  $em = \Drupal::entityTypeManager();
  $user_storage = $em->getStorage('user');

  // Ensure the new role exists. If not, create it first.
  $role_storage = $em->getStorage('user_role');
  if ($role_storage->load('new_role') === NULL) {
    /** @var \Drupal\user\RoleInterface $role */
    $role = $role_storage->create([
      'id' => 'new_role',
      'label' => 'New role',
    ]);
    $role->save();
  }

  $uids = \Drupal::entityQuery('user')
    ->accessCheck(FALSE)
    ->execute();

  if (empty($uids)) {
    return;
  }

  /** @var \Drupal\user\UserInterface[] $users */
  $users = $user_storage->loadMultiple($uids);

  foreach ($users as $user) {
    if ($user->hasRole('old_role')) {
      $user->addRole('new_role');
      $user->removeRole('old_role');
    }

    $user->save();
  }
}
02.04.2026 | Lothar Ferreira Neumann

Updating user roles using a module update hook

my_module.module
use Drupal\views\ViewExecutable;
use Drupal\my_module\Access\NodePageAccessControlHandler;
use Drupal\my_module\Access\RestrictedMediaAccessControlHandler;


/**
 * Implements hook_entity_type_alter().
 */
function my_module_entity_type_alter(array &$entity_types) {
  if (isset($entity_types['node'])) {
    $entity_types['node']->setAccessClass(NodePageAccessControlHandler::class);
  }
  if (isset($entity_types['media'])) {
    $entity_types['media']->setAccessClass(RestrictedMediaAccessControlHandler::class);
  }
}

/**
 * Implements hook_views_post_execute().
 */
function my_module_views_post_execute(ViewExecutable $view) {
  // Only execute if we are on the right view display.
  if ($view->id() === 'MY_VIEW_ID' && $view->current_display === 'MY_VIEW_DISPLAY' ) {

    // Mark this view results as token-allowed in the session for a short time.
    $ttl = 3600; // 1 hour; adjust to taste.
    $now = \Drupal::time()->getRequestTime();
    $session = \Drupal::service('request_stack')->getCurrentRequest()->getSession();
    $grants = (array) $session->get('my_module.token_mids', []);
    foreach ($view->result as $item) {
      $grants[(int) $item->mid] = $now + $ttl;
    }
    $session->set('my_module.token_mids', $grants);
  }
}

NodePageAccessControlHandler.php
namespace Drupal\my_module\Access;

/**
 * Override Node Access Control Handler.
 */
class NodePageAccessControlHandler extends NodeAccessControlHandler {

  ...

  /**
   * {@inheritdoc}
   */
  public function access(EntityInterface $entity, $operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
    if ($operation === 'view') {
      // Mark this node as token-allowed in the session for a short time.
      $ttl = 3600; // 1 hour; adjust to taste.
      $now = $this->time->getRequestTime();
      $session = $this->requestStack->getSession();
      $grants = (array) $session->get('my_module.token_nids', []);
      $grants[(int) $entity->id()] = $now + $ttl;
      $session->set('my_module.token_nids', $grants);
    }
}

RestrictedMediaAccessControlHandler.php
namespace Drupal\my_module\Access;

/**
 * Override Media Access Control Handler.
 */
class RestrictedMediaAccessControlHandler extends MediaAccessControlHandler {

  /**
   * {@inheritdoc
   *
   *  Restrict anonymous view of Media (bundle "restricted_file") to cases where
   *  the user can view the host Node (via paragraph field
   * 'field_download_restricted' or a view listing media entities), 
   * OR they have a valid short-lived token grant for that Node stored in session.
   */
  public function access(EntityInterface $entity, $operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
    if (
      $operation !== 'view'
      || !$account
      || $entity->bundle() !== 'restricted_file'
    ) {
      return parent::access($entity, $operation, $account, $return_as_object);
    }

    $paragraphs = $this->entityTypeManager
      ->getStorage('paragraph')
      ->loadByProperties(['field_download_restricted' => $entity->id()]);

    // Check if we have a media entity id and a cookie for it does exist.
    $media_entity = NULL;
    if (array_key_exists($entity->id(), $mgrants = (array) $this->requestStack->getSession()->get('my_module.token_mids', []))) {
      $media_entity = $this->entityTypeManager
        ->getStorage('media')
        ->loadByProperties(['mid' => $entity->id()]);
      $now = $this->time->getRequestTime();

      // Session-granted access via token.
      $mid = (int) $entity->id();
      if (!empty($mgrants[$mid]) && $mgrants[$mid] >= $now) {
        return AccessResult::allowed()
          ->addCacheableDependency($entity)
          ->addCacheableDependency(reset($media_entity))
          // Important for anon/session-based grants.
          ->mergeCacheMaxAge(0);
      }
    }  

    $result = AccessResult::forbidden()
      ->addCacheableDependency($entity)
      ->cachePerPermissions();

    if (!$paragraphs && !$media_entity) {
      return $result;
    }

    // Read session grants set by my_module_node_access().
    $now = $this->time->getRequestTime();
    $grants = (array) $this->requestStack->getSession()->get('my_module.token_nids', []);

    foreach ($paragraphs as $paragraph) {
      if ($paragraph instanceof ParagraphInterface) {
        $result = $result->addCacheableDependency($paragraph);
      }

      $node = $this->getAncestorEntity($paragraph);
      if (!$node instanceof NodeInterface) {
        continue;
      }

      // Always add the node as a dependency.
      $result = $result->addCacheableDependency($node);

      // Session-granted access via token.
      $nid = (int) $node->id();
      if (!empty($grants[$nid]) && $grants[$nid] >= $now) {
        return AccessResult::allowed()
          ->addCacheableDependency($entity)
          ->addCacheableDependency($node)
          // Important for anon/session-based grants.
          ->mergeCacheMaxAge(0);
      }

      // Existing rule: inherit from node view access.
      if ($node->access('view', $account)) {
        return AccessResult::allowed()
          ->addCacheableDependency($entity)
          ->addCacheableDependency($node)
          ->cachePerPermissions();
      }

      return $result;
    }
  }

}

my_module.module
use Drupal\Core\Form\FormStateInterface;

function my_module_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // Add default values for vapn module so the editor don't need to check this.
  if (isset($form['vapn']) && $form_state->getFormObject()->getEntity()->isNew()) {
    $form['vapn']['content']['widget']['#default_value'] = ['anonymous', 'authenticated'];
  }
}
my_module.deploy.php
use Drupal\node\Entity\Node;

function my_module_deploy_vapn_install() {
  $entity_type_manager = \Drupal::service('entity_type.manager');
  $role_storage = $entity_type_manager->getStorage('user_role');
  $node_storage = $entity_type_manager->getStorage('node');
  $nodes = $node_storage->loadByProperties([
    'type' => 'my_bundle'
  ]);

  $roles = array_values($role_storage->loadMultiple(['anonymous', 'authenticated']));
  foreach ($nodes as $node) {
    // First check if already some roles are set. Only needed if you are upgrading vapn.
    if ($node->vapn->isEmpty()) {
      $node->setSyncing(TRUE)
        ->set('vapn', $roles)
        ->save();
    }
  }
}