From 0fb94c956bade6c3d9ce6157a45822e984ac2731 Mon Sep 17 00:00:00 2001 From: Kristiaan Van den Eynde Date: Tue, 31 May 2016 10:58:34 +0200 Subject: [PATCH] Issue #777578 by hefox, kristiaanvandeneynde: Expand the node access API to a general entity access API to improve security --- core/core.services.yml | 5 + .../Core/Entity/EntityGrantDatabaseStorage.php | 360 +++++++++++++++++++++ .../Entity/EntityGrantDatabaseStorageInterface.php | 153 +++++++++ core/modules/node/node.module | 6 +- core/modules/node/node.services.yml | 5 - core/modules/node/src/Entity/Node.php | 4 +- core/modules/node/src/NodeAccessControlHandler.php | 31 +- core/modules/node/src/NodeGrantDatabaseStorage.php | 308 ------------------ .../node/src/NodeGrantDatabaseStorageInterface.php | 127 -------- .../node/src/Tests/NodeAccessGrantsTest.php | 4 +- .../src/Tests/NodeAccessRebuildNodeGrantsTest.php | 12 +- core/modules/system/system.install | 160 +++++++++ 12 files changed, 713 insertions(+), 462 deletions(-) create mode 100644 core/lib/Drupal/Core/Entity/EntityGrantDatabaseStorage.php create mode 100644 core/lib/Drupal/Core/Entity/EntityGrantDatabaseStorageInterface.php delete mode 100644 core/modules/node/src/NodeGrantDatabaseStorage.php delete mode 100644 core/modules/node/src/NodeGrantDatabaseStorageInterface.php diff --git a/core/core.services.yml b/core/core.services.yml index 6049d3b..dcf3157 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -566,6 +566,11 @@ services: entity.autocomplete_matcher: class: Drupal\Core\Entity\EntityAutocompleteMatcher arguments: ['@plugin.manager.entity_reference_selection'] + entity.grant_storage: + class: Drupal\Core\Entity\EntityGrantDatabaseStorage + arguments: ['@database', '@module_handler', '@entity_type.manager', '@language_manager'] + tags: + - { name: backend_overridable } plugin.manager.entity_reference_selection: class: Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager parent: default_plugin_manager diff --git a/core/lib/Drupal/Core/Entity/EntityGrantDatabaseStorage.php b/core/lib/Drupal/Core/Entity/EntityGrantDatabaseStorage.php new file mode 100644 index 0000000..290fc8d --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityGrantDatabaseStorage.php @@ -0,0 +1,360 @@ +database = $database; + $this->moduleHandler = $module_handler; + $this->entityTypeManager = $entity_type_manager; + $this->languageManager = $language_manager; + } + + /** + * Finds out whether any module defines grants for the the subject entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity for which to check grant implementations. + * + * @return bool + * Whether there are any grant implementations. + */ + protected function hasGrantImplementations(ContentEntityInterface $entity) { + // Short-circuit if any module implements the entity-specific grants hook. + if ($this->moduleHandler->getImplementations($entity->getEntityTypeId() . '_grants')) { + return TRUE; + } + + // Otherwise we need to invoke all generic grants hooks one by one and see + // if any one of them defines grants for the subject entity. We avoid the + // use of invokeAll() for performance reasons. + foreach ($this->moduleHandler->getImplementations('entity_grants') as $module) { + $grants = $this->moduleHandler->invoke($module, 'entity_grants'); + if (!empty($grants[$entity->getEntityTypeId()])) { + return TRUE; + } + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function access(ContentEntityInterface $entity, $operation, AccountInterface $account, $include_default = FALSE) { + // Grants only support these operations. + if (!in_array($operation, ['view', 'update', 'delete'])) { + return AccessResult::neutral(); + } + + // If no module implements the hook or the entity does not have an ID there + // is no point in querying the database for access grants. + if (!$this->hasGrantImplementations($entity) || !$entity->id()) { + return FALSE; + } + + // Check the database for potential access grants. + $query = $this->database->select('entity_access'); + $query->addExpression('1'); + // Only interested for granting in the entity's entity type. + $query->condition('entity_type_id', $entity->getEntityTypeId()); + // Only interested for granting in the current operation. + $query->condition('grant_' . $operation, 1, '>='); + // Check for grants for this entity and the correct langcode. + $entity_ids = $query->andConditionGroup() + ->condition('entity_id', $entity->id()) + ->condition('langcode', $entity->language()->getId()); + // Include the default zero-ID grant if asked to do so. + if ($include_default) { + $entity_ids = $query->orConditionGroup() + ->condition($entity_ids) + ->condition('entity_id', 0); + } + $query->condition($entity_ids); + $query->range(0, 1); + + $grants = static::buildGrantsQueryCondition(node_access_grants($operation, $account)); + + if (count($grants) > 0) { + $query->condition($grants); + } + + // Only the 'view' entity grant can currently be cached; the others don't + // have any cacheability metadata. Hopefully, we can add that in the future, + // which would allow this access check result to be cacheable in all cases. + // For now, this must remain marked as uncacheable, even when it is + // theoretically cacheable, because we don't have the necessary metadata to + // know it for a fact. + $set_cacheability = function (AccessResult $access_result) use ($operation) { + $access_result->addCacheContexts(['user.node_grants:' . $operation]); // @todo user.entity_grants context + if ($operation !== 'view') { + $access_result->setCacheMaxAge(0); + } + return $access_result; + }; + + if ($query->execute()->fetchField()) { + return $set_cacheability(AccessResult::allowed()); + } + else { + return $set_cacheability(AccessResult::neutral()); + } + } + + /** + * {@inheritdoc} + */ + public function checkAll($entity_type_id, AccountInterface $account) { + $query = $this->database->select('entity_access'); + $query->addExpression('COUNT(*)'); + $query + ->condition('entity_type_id', $entity_type_id) + ->condition('entity_id', 0) + ->condition('grant_view', 1, '>='); + + $grants = static::buildGrantsQueryCondition(node_access_grants('view', $account)); //@todo + + if (count($grants) > 0 ) { + $query->condition($grants); + } + return $query->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function alterQuery($entity_type_id, $query, array $tables, $op, AccountInterface $account, $base_table) { + if (!$langcode = $query->getMetaData('langcode')) { + $langcode = FALSE; + } + + // Find all instances of the base table being joined -- could appear + // more than once in the query, and could be aliased. Join each one to + // the node_access table. + $grants = node_access_grants($op, $account); // @todo + foreach ($tables as $alias => $tableinfo) { + $table = $tableinfo['table']; + if (!($table instanceof SelectInterface) && $table == $base_table) { + // Set the subquery. + $subquery = $this->database->select('entity_access', 'ea') + ->condition('entity_type_id', $entity_type_id) + ->fields('ea', array('entity_id')); + + // If any grant exists for the specified user, then user has access to the + // node for the specified operation. + $grant_conditions = static::buildGrantsQueryCondition($grants); + + // Attach conditions to the subquery for nodes. + if (count($grant_conditions->conditions())) { + $subquery->condition($grant_conditions); + } + $subquery->condition('ea.grant_' . $op, 1, '>='); + + // Add langcode-based filtering if this is a multilingual site. + if (\Drupal::languageManager()->isMultilingual()) { + // If no specific langcode to check for is given, use the grant entry + // which is set as a fallback. + if ($langcode === FALSE) { + $subquery->condition('ea.fallback', 1, '='); + } + // If a specific langcode is given, use the grant entry for it. + else { + $subquery->condition('ea.langcode', $langcode, '='); + } + } + + // Now handle entities. @todo allow field to be chosen? + $field = $this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'); + $subquery->where("$alias.$field = ea.entity_id"); + + $query->exists($subquery); + } + } + } + + /** + * {@inheritdoc} + */ + public function write(ContentEntityInterface $entity, array $grants, $realm = NULL, $delete = TRUE) { + if ($delete) { + $query = $this->database->delete('entity_access') + ->condition('entity_type_id', $entity->getEntityTypeId()) + ->condition('entity_id', $entity->id()); + if ($realm) { + $query->condition('realm', array($realm, 'all'), 'IN'); + } + $query->execute(); + } + + // Only perform work when entity_access modules are active. //@todo + if (!empty($grants) && count($this->moduleHandler->getImplementations('node_grants'))) { + $query = $this->database->insert('node_access')->fields(array('entity_type_id', 'entity_id', 'langcode', 'fallback', 'realm', 'gid', 'grant_view', 'grant_update', 'grant_delete')); + // If we have defined a granted langcode, use it. But if not, add a grant + // for every language this entity is translated to. + foreach ($grants as $grant) { + if ($realm && $realm != $grant['realm']) { + continue; + } + if (isset($grant['langcode'])) { + $grant_languages = array($grant['langcode'] => $this->languageManager->getLanguage($grant['langcode'])); + } + else { + $grant_languages = $entity->getTranslationLanguages(TRUE); + } + foreach ($grant_languages as $grant_langcode => $grant_language) { + // Only write grants; denies are implicit. + if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) { + $grant['entity_type_id'] = $entity->getEntityTypeId(); + $grant['entity_type_id'] = $entity->id(); + $grant['langcode'] = $grant_langcode; + // The record with the original langcode is used as the fallback. + if ($grant['langcode'] == $entity->language()->getId()) { + $grant['fallback'] = 1; + } + else { + $grant['fallback'] = 0; + } + $query->values($grant); + } + } + } + $query->execute(); + } + } + + /** + * {@inheritdoc} + */ + public function delete($entity_type_id = NULL) { + if ($entity_type_id) { + $this->database->delete('entity_access')->condition('entity_type_id', $entity_type_id)->execute(); + } + else { + $this->database->truncate('entity_access')->execute(); + } + } + + /** + * {@inheritdoc} + */ + public function writeDefault($entity_type_id) { + $this->database->insert('entity_access') + ->fields(array( + 'entity_type_id' => $entity_type_id, + 'entity_id' => 0, + 'realm' => 'all', + 'gid' => 0, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + )) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function count($entity_type_id = NULL) { + if ($entity_type_id) { + return $this->database->query('SELECT COUNT(*) FROM {entity_access} WHERE entity_type_id = :entity_type_id', [':entity_type_id' => $entity_type_id])->fetchField(); + } + return $this->database->query('SELECT COUNT(*) FROM {entity_access}')->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function deleteAccessRecords($entity_type_id, array $entity_ids) { + $this->database->delete('entity_access') + ->condition('entity_type_id', $entity_type_id) + ->condition('entity_id', $entity_ids, 'IN') + ->execute(); + } + + /** + * Creates a query condition from an array of entity access grants. + * + * @param array $entity_access_grants + * An array of grants, as returned by node_access_grants(). @todo + * @return \Drupal\Core\Database\Query\Condition + * A condition object to be passed to $query->condition(). + * + * @see node_access_grants() @todo + */ + protected static function buildGrantsQueryCondition(array $entity_access_grants) { + $grants = new Condition("OR"); + foreach ($entity_access_grants as $realm => $gids) { + if (!empty($gids)) { + $and = new Condition('AND'); + $grants->condition($and + ->condition('gid', $gids, 'IN') + ->condition('realm', $realm) + ); + } + } + + return $grants; + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityGrantDatabaseStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityGrantDatabaseStorageInterface.php new file mode 100644 index 0000000..470f89b --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityGrantDatabaseStorageInterface.php @@ -0,0 +1,153 @@ +alterQuery($query, $tables, $op, $account, $base_table); + \Drupal::service('entity.grant_storage')->alterQuery('node', $query, $tables, $op, $account, $base_table); // Bubble the 'user.node_grants:$op' cache context to the current render // context. @@ -1181,7 +1181,7 @@ function node_access_rebuild($batch_mode = FALSE) { // loads successfully. if (!empty($node)) { $grants = $access_control_handler->acquireGrants($node); - \Drupal::service('node.grant_storage')->write($node, $grants); + \Drupal::service('entity.grant_storage')->write($node, $grants); } } } @@ -1239,7 +1239,7 @@ function _node_access_rebuild_batch_operation(&$context) { /** @var \Drupal\node\NodeAccessControlHandlerInterface $access_control_handler */ $access_control_handler = \Drupal::entityManager()->getAccessControlHandler('node'); $grants = $access_control_handler->acquireGrants($node); - \Drupal::service('node.grant_storage')->write($node, $grants); + \Drupal::service('entity.grant_storage')->write($node, $grants); } $context['sandbox']['progress']++; $context['sandbox']['current_node'] = $nid; diff --git a/core/modules/node/node.services.yml b/core/modules/node/node.services.yml index 2ff32c3..60d0083 100644 --- a/core/modules/node/node.services.yml +++ b/core/modules/node/node.services.yml @@ -3,11 +3,6 @@ services: class: Drupal\node\Routing\RouteSubscriber tags: - { name: event_subscriber } - node.grant_storage: - class: Drupal\node\NodeGrantDatabaseStorage - arguments: ['@database', '@module_handler', '@language_manager'] - tags: - - { name: backend_overridable } access_check.node.revision: class: Drupal\node\Access\NodeRevisionAccessCheck arguments: ['@entity.manager'] diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index 51794be..e57e1c4 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -135,7 +135,7 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { /** @var \Drupal\node\NodeAccessControlHandlerInterface $access_control_handler */ $access_control_handler = \Drupal::entityManager()->getAccessControlHandler('node'); $grants = $access_control_handler->acquireGrants($this); - \Drupal::service('node.grant_storage')->write($this, $grants, NULL, $update); + \Drupal::service('entity.grant_storage')->write($this, $grants, NULL, $update); } // Reindex the node when it is updated. The node is automatically indexed @@ -164,7 +164,7 @@ public static function preDelete(EntityStorageInterface $storage, array $entitie */ public static function postDelete(EntityStorageInterface $storage, array $nodes) { parent::postDelete($storage, $nodes); - \Drupal::service('node.grant_storage')->deleteNodeRecords(array_keys($nodes)); + \Drupal::service('entity.grant_storage')->deleteAccessRecords('node', array_keys($nodes)); } /** diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php index 988de9e..006e446 100644 --- a/core/modules/node/src/NodeAccessControlHandler.php +++ b/core/modules/node/src/NodeAccessControlHandler.php @@ -8,6 +8,7 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Entity\EntityAccessControlHandler; +use Drupal\Core\Entity\EntityGrantDatabaseStorageInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -21,9 +22,9 @@ class NodeAccessControlHandler extends EntityAccessControlHandler implements NodeAccessControlHandlerInterface, EntityHandlerInterface { /** - * The node grant storage. + * The entity grant storage. * - * @var \Drupal\node\NodeGrantDatabaseStorageInterface + * @var \Drupal\Core\Entity\EntityGrantDatabaseStorageInterface */ protected $grantStorage; @@ -32,10 +33,10 @@ class NodeAccessControlHandler extends EntityAccessControlHandler implements Nod * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * The entity type definition. - * @param \Drupal\node\NodeGrantDatabaseStorageInterface $grant_storage - * The node grant storage. + * @param \Drupal\Core\Entity\EntityGrantDatabaseStorageInterface $grant_storage + * The entity grant storage. */ - public function __construct(EntityTypeInterface $entity_type, NodeGrantDatabaseStorageInterface $grant_storage) { + public function __construct(EntityTypeInterface $entity_type, EntityGrantDatabaseStorageInterface $grant_storage) { parent::__construct($entity_type); $this->grantStorage = $grant_storage; } @@ -46,7 +47,7 @@ public function __construct(EntityTypeInterface $entity_type, NodeGrantDatabaseS public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $entity_type, - $container->get('node.grant_storage') + $container->get('entity.grant_storage') ); } @@ -103,8 +104,18 @@ protected function checkAccess(EntityInterface $node, $operation, AccountInterfa return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($node); } - // Evaluate node grants. - return $this->grantStorage->access($node, $operation, $account); + // Evaluate entity grants, including the default zero-ID grant if the node + // is published. + $access = $this->grantStorage->access($node, $operation, $account, $status); + + // If there were no modules defining node grants, allow view access if the + // node is published. + if ($access === FALSE && $operation === 'view') { + return AccessResult::allowedIf($status)->addCacheableDependency($node); + } + + // Otherwise return whatever the grants returned. + return $access; } /** @@ -168,7 +179,7 @@ public function writeGrants(NodeInterface $node, $delete = TRUE) { * {@inheritdoc} */ public function writeDefaultGrant() { - $this->grantStorage->writeDefault(); + $this->grantStorage->writeDefault('node'); } /** @@ -189,7 +200,7 @@ public function countGrants() { * {@inheritdoc} */ public function checkAllGrants(AccountInterface $account) { - return $this->grantStorage->checkAll($account); + return $this->grantStorage->checkAll('node', $account); } } diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php deleted file mode 100644 index a0a1337..0000000 --- a/core/modules/node/src/NodeGrantDatabaseStorage.php +++ /dev/null @@ -1,308 +0,0 @@ -database = $database; - $this->moduleHandler = $module_handler; - $this->languageManager = $language_manager; - } - - /** - * {@inheritdoc} - */ - public function access(NodeInterface $node, $operation, AccountInterface $account) { - // Grants only support these operations. - if (!in_array($operation, ['view', 'update', 'delete'])) { - return AccessResult::neutral(); - } - - // If no module implements the hook or the node does not have an id there is - // no point in querying the database for access grants. - if (!$this->moduleHandler->getImplementations('node_grants') || !$node->id()) { - // Return the equivalent of the default grant, defined by - // self::writeDefault(). - if ($operation === 'view') { - return AccessResult::allowedIf($node->isPublished())->addCacheableDependency($node); - } - else { - return AccessResult::neutral(); - } - } - - // Check the database for potential access grants. - $query = $this->database->select('node_access'); - $query->addExpression('1'); - // Only interested for granting in the current operation. - $query->condition('grant_' . $operation, 1, '>='); - // Check for grants for this node and the correct langcode. - $nids = $query->andConditionGroup() - ->condition('nid', $node->id()) - ->condition('langcode', $node->language()->getId()); - // If the node is published, also take the default grant into account. The - // default is saved with a node ID of 0. - $status = $node->isPublished(); - if ($status) { - $nids = $query->orConditionGroup() - ->condition($nids) - ->condition('nid', 0); - } - $query->condition($nids); - $query->range(0, 1); - - $grants = static::buildGrantsQueryCondition(node_access_grants($operation, $account)); - - if (count($grants) > 0) { - $query->condition($grants); - } - - // Only the 'view' node grant can currently be cached; the others currently - // don't have any cacheability metadata. Hopefully, we can add that in the - // future, which would allow this access check result to be cacheable in all - // cases. For now, this must remain marked as uncacheable, even when it is - // theoretically cacheable, because we don't have the necessary metadata to - // know it for a fact. - $set_cacheability = function (AccessResult $access_result) use ($operation) { - $access_result->addCacheContexts(['user.node_grants:' . $operation]); - if ($operation !== 'view') { - $access_result->setCacheMaxAge(0); - } - return $access_result; - }; - - if ($query->execute()->fetchField()) { - return $set_cacheability(AccessResult::allowed()); - } - else { - return $set_cacheability(AccessResult::neutral()); - } - } - - /** - * {@inheritdoc} - */ - public function checkAll(AccountInterface $account) { - $query = $this->database->select('node_access'); - $query->addExpression('COUNT(*)'); - $query - ->condition('nid', 0) - ->condition('grant_view', 1, '>='); - - $grants = static::buildGrantsQueryCondition(node_access_grants('view', $account)); - - if (count($grants) > 0 ) { - $query->condition($grants); - } - return $query->execute()->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function alterQuery($query, array $tables, $op, AccountInterface $account, $base_table) { - if (!$langcode = $query->getMetaData('langcode')) { - $langcode = FALSE; - } - - // Find all instances of the base table being joined -- could appear - // more than once in the query, and could be aliased. Join each one to - // the node_access table. - $grants = node_access_grants($op, $account); - foreach ($tables as $nalias => $tableinfo) { - $table = $tableinfo['table']; - if (!($table instanceof SelectInterface) && $table == $base_table) { - // Set the subquery. - $subquery = $this->database->select('node_access', 'na') - ->fields('na', array('nid')); - - // If any grant exists for the specified user, then user has access to the - // node for the specified operation. - $grant_conditions = static::buildGrantsQueryCondition($grants); - - // Attach conditions to the subquery for nodes. - if (count($grant_conditions->conditions())) { - $subquery->condition($grant_conditions); - } - $subquery->condition('na.grant_' . $op, 1, '>='); - - // Add langcode-based filtering if this is a multilingual site. - if (\Drupal::languageManager()->isMultilingual()) { - // If no specific langcode to check for is given, use the grant entry - // which is set as a fallback. - // If a specific langcode is given, use the grant entry for it. - if ($langcode === FALSE) { - $subquery->condition('na.fallback', 1, '='); - } - else { - $subquery->condition('na.langcode', $langcode, '='); - } - } - - $field = 'nid'; - // Now handle entities. - $subquery->where("$nalias.$field = na.nid"); - - $query->exists($subquery); - } - } - } - - /** - * {@inheritdoc} - */ - public function write(NodeInterface $node, array $grants, $realm = NULL, $delete = TRUE) { - if ($delete) { - $query = $this->database->delete('node_access')->condition('nid', $node->id()); - if ($realm) { - $query->condition('realm', array($realm, 'all'), 'IN'); - } - $query->execute(); - } - // Only perform work when node_access modules are active. - if (!empty($grants) && count($this->moduleHandler->getImplementations('node_grants'))) { - $query = $this->database->insert('node_access')->fields(array('nid', 'langcode', 'fallback', 'realm', 'gid', 'grant_view', 'grant_update', 'grant_delete')); - // If we have defined a granted langcode, use it. But if not, add a grant - // for every language this node is translated to. - foreach ($grants as $grant) { - if ($realm && $realm != $grant['realm']) { - continue; - } - if (isset($grant['langcode'])) { - $grant_languages = array($grant['langcode'] => $this->languageManager->getLanguage($grant['langcode'])); - } - else { - $grant_languages = $node->getTranslationLanguages(TRUE); - } - foreach ($grant_languages as $grant_langcode => $grant_language) { - // Only write grants; denies are implicit. - if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) { - $grant['nid'] = $node->id(); - $grant['langcode'] = $grant_langcode; - // The record with the original langcode is used as the fallback. - if ($grant['langcode'] == $node->language()->getId()) { - $grant['fallback'] = 1; - } - else { - $grant['fallback'] = 0; - } - $query->values($grant); - } - } - } - $query->execute(); - } - } - - /** - * {@inheritdoc} - */ - public function delete() { - $this->database->truncate('node_access')->execute(); - } - - /** - * {@inheritdoc} - */ - public function writeDefault() { - $this->database->insert('node_access') - ->fields(array( - 'nid' => 0, - 'realm' => 'all', - 'gid' => 0, - 'grant_view' => 1, - 'grant_update' => 0, - 'grant_delete' => 0, - )) - ->execute(); - } - - /** - * {@inheritdoc} - */ - public function count() { - return $this->database->query('SELECT COUNT(*) FROM {node_access}')->fetchField(); - } - - /** - * {@inheritdoc} - */ - public function deleteNodeRecords(array $nids) { - $this->database->delete('node_access') - ->condition('nid', $nids, 'IN') - ->execute(); - } - - /** - * Creates a query condition from an array of node access grants. - * - * @param array $node_access_grants - * An array of grants, as returned by node_access_grants(). - * @return \Drupal\Core\Database\Query\Condition - * A condition object to be passed to $query->condition(). - * - * @see node_access_grants() - */ - protected static function buildGrantsQueryCondition(array $node_access_grants) { - $grants = new Condition("OR"); - foreach ($node_access_grants as $realm => $gids) { - if (!empty($gids)) { - $and = new Condition('AND'); - $grants->condition($and - ->condition('gid', $gids, 'IN') - ->condition('realm', $realm) - ); - } - } - - return $grants; - } - -} diff --git a/core/modules/node/src/NodeGrantDatabaseStorageInterface.php b/core/modules/node/src/NodeGrantDatabaseStorageInterface.php deleted file mode 100644 index 659ce10..0000000 --- a/core/modules/node/src/NodeGrantDatabaseStorageInterface.php +++ /dev/null @@ -1,127 +0,0 @@ -assertTrue(\Drupal::service('node.grant_storage')->access($node, 'view', $this->webUser), 'The expected node access records are present'); - $this->assertEqual(1, \Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is an all realm access record'); + $this->assertTrue(\Drupal::service('entity.grant_storage')->access($node, 'view', $this->webUser), 'The expected node access records are present'); + $this->assertEqual(1, \Drupal::service('entity.grant_storage')->checkAll('node', $this->webUser), 'There is an all realm access record'); $this->assertTrue(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions need to be rebuilt'); // Rebuild permissions. @@ -52,8 +52,8 @@ public function testNodeAccessRebuildNodeGrants() { // Test if the rebuild has been successful. $this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions have been rebuilt'); - $this->assertTrue(\Drupal::service('node.grant_storage')->access($node, 'view', $this->webUser), 'The expected node access records are present'); - $this->assertFalse(\Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is no all realm access record'); + $this->assertTrue(\Drupal::service('entity.grant_storage')->access($node, 'view', $this->webUser), 'The expected node access records are present'); + $this->assertFalse(\Drupal::service('entity.grant_storage')->checkAll('node', $this->webUser), 'There is no all realm access record'); } /** @@ -61,7 +61,7 @@ public function testNodeAccessRebuildNodeGrants() { */ public function testNodeAccessRebuildNoAccessModules() { // Default realm access is present. - $this->assertEqual(1, \Drupal::service('node.grant_storage')->count(), 'There is an all realm access record'); + $this->assertEqual(1, \Drupal::service('entity.grant_storage')->count('node'), 'There is an all realm access record'); // No need to rebuild permissions. $this->assertFalse(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions need to be rebuilt'); @@ -74,7 +74,7 @@ public function testNodeAccessRebuildNoAccessModules() { $this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions have been rebuilt'); // Default realm access is still present. - $this->assertEqual(1, \Drupal::service('node.grant_storage')->count(), 'There is an all realm access record'); + $this->assertEqual(1, \Drupal::service('node.grant_storage')->count('node'), 'There is an all realm access record'); } } diff --git a/core/modules/system/system.install b/core/modules/system/system.install index cdaeba6..a316b54 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -988,6 +988,79 @@ function system_schema() { // thrown every request until an alias is created. $schema['url_alias'] = AliasStorage::schemaDefinition(); + $schema['entity_access'] = array( + 'description' => 'Identifies which realm/grant pairs a user must possess in order to view, update, or delete specific content entities.', + 'fields' => array( + 'entity_type_id' => array( + 'description' => "The entity type ID this record affects.", + 'type' => 'varchar_ascii', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'entity_id' => array( + 'description' => 'The entity ID this record affects.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'langcode' => array( + 'description' => 'The {language}.langcode of this entity.', + 'type' => 'varchar_ascii', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + 'fallback' => array( + 'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 1, + ), + 'gid' => array( + 'description' => "The grant ID a user must possess in the specified realm to gain this row's privileges on the entity.", + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'realm' => array( + 'description' => 'The realm in which the user must possess the grant ID. Each entity access function can define one or more realms.', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'grant_view' => array( + 'description' => 'Boolean indicating whether a user with the realm/grant pair can view this entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + 'grant_update' => array( + 'description' => 'Boolean indicating whether a user with the realm/grant pair can edit this entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + 'grant_delete' => array( + 'description' => 'Boolean indicating whether a user with the realm/grant pair can delete this entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + ), + 'primary key' => array('entity_type_id', 'entity_id', 'gid', 'realm', 'langcode'), + ); + return $schema; } @@ -1652,3 +1725,90 @@ function system_update_8014() { /** * @} End of "addtogroup updates-8.0.0-rc". */ + +/** + * @addtogroup updates-8.2.0-rc + * @{ + */ + +/** + * Creates the {entity_access} table. + */ +function system_update_8015() { + db_create_table('entity_access', array( + 'description' => 'Identifies which realm/grant pairs a user must possess in order to view, update, or delete specific content entities.', + 'fields' => array( + 'entity_type_id' => array( + 'description' => "The entity type ID this record affects.", + 'type' => 'varchar_ascii', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + ), + 'entity_id' => array( + 'description' => 'The entity ID this record affects.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'langcode' => array( + 'description' => 'The {language}.langcode of this entity.', + 'type' => 'varchar_ascii', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ), + 'fallback' => array( + 'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 1, + ), + 'gid' => array( + 'description' => "The grant ID a user must possess in the specified realm to gain this row's privileges on the entity.", + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'realm' => array( + 'description' => 'The realm in which the user must possess the grant ID. Each entity access function can define one or more realms.', + 'type' => 'varchar_ascii', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'grant_view' => array( + 'description' => 'Boolean indicating whether a user with the realm/grant pair can view this entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + 'grant_update' => array( + 'description' => 'Boolean indicating whether a user with the realm/grant pair can edit this entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + 'grant_delete' => array( + 'description' => 'Boolean indicating whether a user with the realm/grant pair can delete this entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + ), + 'primary key' => array('entity_type_id', 'entity_id', 'gid', 'realm', 'langcode'), + )); +} + +/** + * @} End of "addtogroup updates-8.2.0-rc". + */ -- 2.4.9 (Apple Git-60)