Change record status: 
Project: 
Description: 

From this moment, OS doesn't allow adding or using any hook_node_grants() to control access to nodes.

There are a few reasons why we should get rid of node grants:

  • The patch is a hard dependency for gnode module
  • Some node grants providers are very heavy (for example, entity_access_by_field_node_grants_alter())
  • Uncachable pages (user.node_grants:view cache context is unique per user (the main reason is entity_access_by_field_node_grants()))

If hook_node_grants() is added to the code, the appropriate notification will be highlighted on Status page, and (what is more important to know ☝️) the results of any query for nodes will be broken.

We added the alternative approach with query builders. In OS all related hook_node_grants() hooks were replaced by event subscribers. The node query building flow looks like
alt

So, what was changed in general?

  • All hook_node_grants() and related were removed
  • \Drupal\social_node\QueryAccess\NodeEntityQueryAlter::alterQuery() was added as the entry point for altering the node query
  • Event subscribers added to determine the access to nodes:
    • \Drupal\social_course\EventSubscriber\NodeQueryAccessAlterSubscriber::alterQueryAccess()
    • \Drupal\social_event_managers\EventSubscriber\NodeQueryAccessAlterSubscriber::alterQueryAccess()
    • \Drupal\social_group\EventSubscriber\NodeQueryAccessAlterSubscriber::alterQueryAccess()
    • \Drupal\social_node\EventSubscriber\NodeQueryAccessAlterSubscriber::alterQueryAccess()
  • entity_access_by_field and content_access search API processors removed from views
  • \Drupal\social_search\Plugin\search_api\processor\TaggingQuery added allows altering search queries
  • A list of new search API processors added to alter search queries for nodes:
    • \Drupal\social_event_managers\Plugin\search_api\processor\SearchApiQueryAlter::preprocessSearchQuery()
    • \Drupal\social_group\Plugin\search_api\processor\SearchApiQueryAlter::preprocessSearchQuery()
    • \Drupal\social_node\Plugin\search_api\processor\SearchApiQueryAlter::preprocessSearchQuery()

What should I do if I have custom hooks with node grants?
First, replace each hook with an appropriate event subscriber. We recommend to use \Drupal\social_node\SocialNodeQueryAccessAlterInterface
A very common implementation could look like:

<?php

declare(strict_types=1);

namespace Drupal\my_module\EventSubscriber;

use Drupal\social_node\Event\NodeQueryAccessEvent;
use Drupal\social_node\Event\SocialNodeEvents;
use Drupal\social_node\SocialNodeQueryAccessAlterInterface;

class NodeQueryAccessAlterSubscriber implements SocialNodeQueryAccessAlterInterface {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    if (!class_exists(SocialNodeEvents::class)) {
      return [];
    }

    $events[SocialNodeEvents::NODE_QUERY_ACCESS_ALTER][] = ['alterQueryAccess'];
    return $events;
  }

  /**
   * {@inheritdoc}
   */
  public function alterQueryAccess(NodeQueryAccessEvent $event): void {
    $account = $event->account();
    $or = $event->getConditions();

    // Make sure a user is a moderator.
    $moderators_table = $event->ensureNodeFieldTableJoin('field_moderators');
    $or->condition("$moderators_table.field_moderators_target_id", $account->id());
  }

}

In case I'm using search API need to create a new search API processor. We don't have the interface for it but in general, the implementation could look like

<?php

declare(strict_types=1);

namespace Drupal\my_module\Plugin\search_api\processor;

use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\search_api\LoggerTrait;
use Drupal\search_api\Processor\ProcessorPluginBase;
use Drupal\search_api\Query\ConditionGroupInterface;
use Drupal\search_api\Query\QueryInterface;
use Drupal\social_search\Plugin\search_api\SocialSearchSearchApiProcessorTrait;
use Drupal\social_search\Utility\SocialSearchApi;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * @SearchApiProcessor(
 *   id = "my_module_query_alter",
 *   stages = {
 *     "pre_index_save" = 0,
 *     "preprocess_query" = 100,
 *   },
 *   locked = true,
 *   hidden = true,
 * )
 */
class SearchApiQueryAlter extends ProcessorPluginBase {

  use LoggerTrait;
  use SocialSearchSearchApiProcessorTrait;

  /**
   * {@inheritdoc}
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    protected EntityFieldManagerInterface $entityFieldManager,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_field.manager'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public static function getIndexData(): array {
    return [
      'node' => [
        'field_moderators' => ['type' => 'integer'],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function preprocessSearchQuery(QueryInterface $query): void {
    /* @see \Drupal\social_search\Plugin\search_api\processor\TaggingQuery::preprocessSearchQuery() */
    $or = SocialSearchApi::findTaggedQueryConditionsGroup('social_entity_type_node_access', $query->getConditionGroup());
    if (!$or instanceof ConditionGroupInterface) {
      return;
    }

    $account = $query->getOption('social_search_access_account');
    
    $field_event_managers = $this->findField('entity:node', 'field_moderators');
    if (!$field_event_managers) {
      // The field doesn't exist in the index.
      return;
    }

    $or->addCondition($field_event_managers->getFieldIdentifier(), $account->id());
  }

}
AttachmentSize
Screenshot 2025-02-19 at 13.46.30.png137.62 KB
Impacts: 
Module developers