Skip to content

Commit

Permalink
Issue #3458767 by SV: Add QueryAccess handlers for group_content
Browse files Browse the repository at this point in the history
  • Loading branch information
volodymyr-sydor committed Sep 25, 2024
1 parent ffa8b5f commit 3b2a0de
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ use Drupal\entity_access_by_field\EntityAccessHelper;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\NodeInterface;
use Drupal\social_post\Entity\PostInterface;
use Drupal\entity_access_by_field\QueryAccess\GroupContentQueryAlter;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\Query\AlterableInterface;

/**
* Here we define a constant for our node access grant ID.
Expand Down Expand Up @@ -212,3 +215,42 @@ function _entity_access_by_field_get_default_visibility(EntityInterface $entity)

return $default_visibility;
}

/**
* Implements hook_query_TAG_alter().
*/
function entity_access_by_field_query_entity_query_alter(AlterableInterface $query): void {

$class_name = '';

if (!$query instanceof SelectInterface) {
return;
}

$entity_type_id = $query->getMetaData('entity_type');
if ($query->hasTag($entity_type_id . '_access')) {
$entity_type_manager = \Drupal::entityTypeManager();

/** @var Drupal\Core\Entity\EntityTypeInterface $entity_type */
$entity_type = $entity_type_manager->getDefinition($entity_type_id);

if ($entity_type_id === 'group_content') {
\Drupal::service('class_resolver')
->getInstanceFromDefinition(GroupContentQueryAlter::class)
->alter($query, $entity_type);
}
}
}

/**
* Implements hook_query_TAG_alter().
*/
function entity_access_by_field_query_views_entity_query_alter(AlterableInterface $query): void {
if (!$query instanceof SelectInterface) {
return;
}

$entity_type_id = $query->getMetaData('entity_type');
$query->addTag($entity_type_id . '_access');
entity_access_by_field_query_entity_query_alter($query);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ services:

entity_access_by_field.query_access_subscriber:
class: Drupal\entity_access_by_field\EventSubscriber\QueryAccessSubscriber
arguments: [ '@current_user', '@database']
tags:
- { name: event_subscriber }
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@

namespace Drupal\entity_access_by_field\EventSubscriber;

use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity\QueryAccess\ConditionGroup;
use Drupal\entity\QueryAccess\QueryAccessEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class QueryAccessSubscriber implements EventSubscriberInterface {

/**
* Constructs QueryAccessSubscriber.
*
* @param \Drupal\Core\Session\AccountInterface $currentUser
* The current user.
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
*/
public function __construct(
protected AccountInterface $currentUser,
protected Connection $database,
) {}

/**
* {@inheritdoc}
*/
Expand All @@ -24,7 +38,7 @@ public static function getSubscribedEvents() {
* @param \Drupal\entity\QueryAccess\QueryAccessEvent $event
* The event.
*/
public function nodeQueryAccess(QueryAccessEvent $event) {
public function nodeQueryAccess(QueryAccessEvent $event): void {
if ($event->getOperation() !== 'view') {
return;
}
Expand All @@ -35,25 +49,44 @@ public function nodeQueryAccess(QueryAccessEvent $event) {
return;
}

$conditions = $event->getConditions();

$accessByVisibility = new ConditionGroup('OR');

// Content is queryable only if the user has permission.
$bundle_info_service = \Drupal::service('entity_type.bundle.info');
foreach ($bundle_info_service->getBundleInfo('node') as $bundle => $info) {
foreach (['public', 'community', 'group'] as $visibility) {
foreach (['public', 'community'] as $visibility) {
if ($account->hasPermission("view node.$bundle.field_content_visibility:$visibility content")) {
$accessByVisibility->addCondition(
(new ConditionGroup())
->addCondition('bundle', $bundle)
->addCondition('field_content_visibility', $visibility)
);
}
}
}
$conditions->addCondition($accessByVisibility);

// Get memberships.
$memberships = $this->database->select('group_relationship_field_data')
->fields('group_relationship_field_data', ['gid'])
->condition('type', '%group_membership%', 'LIKE')
->condition('entity_id', $account->id());

// Get group content.
$nids = $this->database->select('group_relationship_field_data')
->fields('group_relationship_field_data', ['entity_id'])
->condition('type', 'flexible_group-group_node-%', 'LIKE')
->condition('gid', $memberships, 'IN');

// Check if users has an access as a member of group.
$accessByVisibility->addCondition(
(new ConditionGroup())
->addCondition('nid', $nids, 'IN')
);

// Check if user is author.
$accessByVisibility->addCondition('uid', $account->id());

$conditions = $event->getConditions();
$conditions->addCondition($accessByVisibility);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

namespace Drupal\entity_access_by_field\QueryAccess;

use Drupal\group\QueryAccess\GroupRelationshipQueryAlter;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Defines a class for altering group queries.
*
* @todo Revisit cacheability and see if we can optimize some more.
*
* @internal
*/
class GroupContentQueryAlter extends GroupRelationshipQueryAlter {

/**
* The group relation type manager.
*
* @var \Drupal\group\Plugin\Group\Relation\GroupRelationTypeManagerInterface
*/
protected $pluginManager;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): void {
$instance = parent::create($container);
$instance->pluginManager = $container->get('group_relation_type.manager');
return $instance;
}

/**
* {@inheritdoc}
*/
protected function doAlter($operation): void {
$installed_ids = $this->pluginManager->getAllInstalledIds();

// Check if any of the plugins actually support the operation. If not, we
// can simply bail out here to play nice with other modules that do support
// the provided operation.
$operation_is_supported = FALSE;
foreach ($installed_ids as $plugin_id) {
if ($this->pluginManager->getAccessControlHandler($plugin_id)->supportsOperation($operation, 'relationship')) {
$operation_is_supported = TRUE;
break;
}
}

if (!$operation_is_supported) {
return;
}

$tables = $this->query->getTables();
if (!isset($tables['node_field_data_group_relationship_field_data'])) {
return;
}

$account = $this->currentUser;

if ($account->hasPermission('administer nodes')) {
return;
}

$base_table = $this->ensureBaseTable();
$id_key = $this->entityType->getKey('id');

$group_relationship_data_table = 'node__field_content_visibility';
$this->joinAliasPlugins = $this->query->leftJoin(
$group_relationship_data_table,
'nfcv',
"node_field_data_group_relationship_field_data.nid=%alias.entity_id"
);

$group_conditions = $this->query->orConditionGroup();

foreach (['group'] as $visibility) {
if ($account->hasPermission("view node.event.field_content_visibility:$visibility content")) {
$this->query->condition('nfcv.field_content_visibility_value', $visibility);
}
}

// If any new relationship is added, it might change access.
$this->cacheableMetadata->addCacheTags(['group_content_list']);

// Retrieve the full list of group permissions for the user.
$this->cacheableMetadata->addCacheContexts(['user.group_permissions']);
$calculated_permissions = $this->permissionCalculator->calculateFullPermissions($this->currentUser);

$allowed_any_ids = $allowed_own_ids = [];
foreach ($installed_ids as $plugin_id) {
$handler = $this->pluginManager->getPermissionProvider($plugin_id);
$admin_permission = $handler->getAdminPermission();
$any_permission = $handler->getPermission($operation, 'relationship', 'any');
$own_permission = $handler->getPermission($operation, 'relationship', 'own');

foreach ($calculated_permissions->getItems() as $item) {
if ($admin_permission !== FALSE && $item->hasPermission($admin_permission)) {
$allowed_any_ids[$item->getScope()][$plugin_id][] = $item->getIdentifier();
}
elseif ($any_permission !== FALSE && $item->hasPermission($any_permission)) {
$allowed_any_ids[$item->getScope()][$plugin_id][] = $item->getIdentifier();
}
elseif ($own_permission !== FALSE && $item->hasPermission($own_permission)) {
$allowed_own_ids[$item->getScope()][$plugin_id][] = $item->getIdentifier();
}
}
}

// If no group type or group gave access, we deny access altogether.
if (empty($allowed_any_ids) && empty($allowed_own_ids)) {
$this->query->alwaysFalse();
return;
}

// If we only have any IDs or own IDs, we can simply add those conditions
// in their dedicated section below. However, if we have both, we need to
// add both sections to an OR group to avoid two contradicting access checks
// to cancel each other out, leading to no results.
$condition_attacher = $this->query;
if (!empty($allowed_any_ids) && !empty($allowed_own_ids)) {
$condition_attacher = $this->ensureOrConjunction($this->query);
}

if (!empty($allowed_any_ids)) {
$this->addScopedConditions($allowed_any_ids, $condition_attacher);
}

if (!empty($allowed_own_ids)) {
$this->cacheableMetadata->addCacheContexts(['user']);
$data_table = $this->ensureDataTable();

$condition_attacher->condition($owner_conditions = $this->query->andConditionGroup());
$owner_conditions->condition("$data_table.uid", $this->currentUser->id());
$this->addScopedConditions($allowed_own_ids, $owner_conditions);
}
}

/**
* {@inheritdoc}
*/
protected function getPluginDataTable() {
return $this->ensureDataTable();
}

/**
* {@inheritdoc}
*/
protected function getMembershipJoinTable() {
return $this->ensureDataTable();
}

/**
* {@inheritdoc}
*/
protected function getMembershipJoinLeftField() {
return 'gid';
}

}
15 changes: 0 additions & 15 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1427,21 +1427,6 @@ parameters:
count: 1
path: modules/custom/entity_access_by_field/entity_access_by_field.module

-
message: "#^Function entity_access_by_field_node_access_explain\\(\\) has no return type specified\\.$#"
count: 1
path: modules/custom/entity_access_by_field/entity_access_by_field.module

-
message: "#^Function entity_access_by_field_node_access_explain\\(\\) has parameter \\$row with no type specified\\.$#"
count: 1
path: modules/custom/entity_access_by_field/entity_access_by_field.module

-
message: "#^Function entity_access_by_field_node_grants\\(\\) has parameter \\$op with no type specified\\.$#"
count: 1
path: modules/custom/entity_access_by_field/entity_access_by_field.module

-
message: "#^Function entity_access_by_field_node_presave\\(\\) has no return type specified\\.$#"
count: 1
Expand Down

0 comments on commit 3b2a0de

Please sign in to comment.