Commit a042df2e authored by catch's avatar catch

Issue #2172017 by dawehner, larowlan, MegaChriz, kim.pepper, Désiré, Sam...

Issue #2172017 by dawehner, larowlan, MegaChriz, kim.pepper, Désiré, Sam Hermans, tim.plunkett, Antti J. Salminen: Bulk operations does not respect entity access
parent e2e00e91
......@@ -9,6 +9,7 @@
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Executable\ExecutableInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an interface for an Action plugin.
......@@ -44,4 +45,24 @@ interface ActionInterface extends ExecutableInterface, PluginInspectionInterface
*/
public function executeMultiple(array $objects);
/**
* Checks object access.
*
* @param mixed $object
* The object to execute the action on.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE);
}
......@@ -7,11 +7,13 @@
namespace Drupal\action\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\Token;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -184,4 +186,12 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
$this->configuration['message'] = $form_state->getValue('message');
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowed();
return $return_as_object ? $result : $result->isAllowed();
}
}
......@@ -7,10 +7,12 @@
namespace Drupal\action\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
......@@ -113,4 +115,12 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
$this->configuration['url'] = $form_state->getValue('url');
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$access = AccessResult::allowed();
return $return_as_object ? $access : $access->isAllowed();
}
}
......@@ -8,9 +8,11 @@
namespace Drupal\action\Plugin\Action;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Utility\Token;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -89,4 +91,12 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
unset($this->configuration['node']);
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowed();
return $return_as_object ? $result : $result->isAllowed();
}
}
......@@ -63,6 +63,14 @@ public function testBulkForm() {
$edit["node_bulk_form[$i]"] = TRUE;
}
// Log in as a user with 'administer nodes' permission to have access to the
// bulk operation.
$this->drupalCreateContentType(['type' => 'page']);
$admin_user = $this->drupalCreateUser(['administer nodes', 'edit any page content']);
$this->drupalLogin($admin_user);
$this->drupalGet('test_bulk_form');
// Set all nodes to sticky and check that.
$edit += array('action' => 'node_make_sticky_action');
$this->drupalPostForm(NULL, $edit, t('Apply'));
......
......@@ -8,7 +8,7 @@
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\comment\CommentInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Publishes a comment.
......@@ -29,4 +29,15 @@ public function execute($comment = NULL) {
$comment->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
$result = $object->status->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Saves a comment.
......@@ -27,4 +28,12 @@ public function execute($comment = NULL) {
$comment->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
return $object->access('update', $account, $return_as_object);
}
}
......@@ -9,8 +9,8 @@
use Drupal\Component\Utility\Tags;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\comment\CommentInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Unpublishes a comment containing certain keywords.
......@@ -67,4 +67,15 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
$this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords'));
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
$result = $object->access('update', $account, TRUE)
->andIf($object->status->access('edit', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}
......@@ -8,7 +8,7 @@
namespace Drupal\comment\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\comment\CommentInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Unpublishes a comment.
......@@ -29,4 +29,15 @@ public function execute($comment = NULL) {
$comment->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\comment\CommentInterface $object */
$result = $object->status->access('edit', $account, TRUE)
->andIf($object->access('update', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}
......@@ -11,6 +11,7 @@
use Drupal\Core\Database\Connection;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -132,4 +133,15 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
$this->configuration['owner_uid'] = $this->connection->query('SELECT uid from {users_field_data} WHERE name = :name AND default_langcode = 1', array(':name' => $form_state->getValue('owner_name')))->fetchField();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$result = $object->access('update', $account, TRUE)
->andIf($object->uid->access('edit', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}
......@@ -87,4 +87,12 @@ public function execute($object = NULL) {
$this->executeMultiple(array($object));
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
return $object->access('delete', $account, $return_as_object);
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Demotes a node.
......@@ -28,4 +29,15 @@ public function execute($entity = NULL) {
$entity->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$result = $object->access('update', $account, TRUE)
->andIf($object->promote->access('edit', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Promotes a node.
......@@ -24,9 +25,18 @@ class PromoteNode extends ActionBase {
* {@inheritdoc}
*/
public function execute($entity = NULL) {
$entity->setPublished(TRUE);
$entity->setPromoted(TRUE);
$entity->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$access = $object->access('update', $account, TRUE)
->andif($object->promote->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Publishes a node.
......@@ -28,4 +29,15 @@ public function execute($entity = NULL) {
$entity->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$result = $object->access('update', $account, TRUE)
->andIf($object->status->access('edit', $account, TRUE));
return $return_as_object ? $result : $result->isAllowed();
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an action that can save any entity.
......@@ -27,4 +28,12 @@ public function execute($entity = NULL) {
$entity->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
return $object->access('update', $account, $return_as_object);
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Makes a node sticky.
......@@ -24,9 +25,18 @@ class StickyNode extends ActionBase {
* {@inheritdoc}
*/
public function execute($entity = NULL) {
$entity->status = NODE_PUBLISHED;
$entity->sticky = NODE_STICKY;
$entity->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$access = $object->access('update', $account, TRUE)
->andif($object->sticky->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}
......@@ -10,6 +10,7 @@
use Drupal\Component\Utility\Tags;
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Unpublishes a node containing certain keywords.
......@@ -65,4 +66,15 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
$this->configuration['keywords'] = Tags::explode($form_state->getValue('keywords'));
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$access = $object->access('update', $account, TRUE)
->andIf($object->status->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Unpublishes a node.
......@@ -28,4 +29,15 @@ public function execute($entity = NULL) {
$entity->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$access = $object->access('update', $account, TRUE)
->andIf($object->status->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\node\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
/**
* Makes a node not sticky.
......@@ -28,4 +29,15 @@ public function execute($entity = NULL) {
$entity->save();
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $object */
$access = $object->access('update', $account, TRUE)
->andIf($object->sticky->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
}
<?php
/**
* @file
* Contains \Drupal\node\Tests\Views\BulkFormAccessTest.
*/
namespace Drupal\node\Tests\Views;
use Drupal\Component\Utility\String;
use Drupal\node\Entity\Node;
/**
* Tests if entity access is respected on a node bulk operations form.
*
* @group node
* @see \Drupal\node\Plugin\views\field\BulkForm
* @see \Drupal\node\Tests\NodeTestBase
* @see \Drupal\node\Tests\NodeAccessBaseTableTest
* @see \Drupal\node\Tests\Views\BulkFormTest
*/
class BulkFormAccessTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node_test_views', 'node_access_test');
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_node_bulk_form');
/**
* The node access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $accessHandler;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create Article node type.
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
$this->accessHandler = \Drupal::entityManager()->getAccessControlHandler('node');
node_access_test_add_field(entity_load('node_type', 'article'));
// After enabling a node access module, the access table has to be rebuild.
node_access_rebuild();
// Enable the private node feature of the node_access_test module.
\Drupal::state()->set('node_access_test.private', TRUE);
}
/**
* Tests if nodes that may not be edited, can not be edited in bulk.
*/
public function testNodeEditAccess() {
// Create an account who will be the author of a private node.
$author = $this->drupalCreateUser();
// Create a private node (author may view, edit and delete, others may not).
$node = $this->drupalCreateNode(array(
'type' => 'article',
'private' => array(array(
'value' => TRUE,
)),
'uid' => $author->id(),
));
// Create an account that may view the private node, but not edit it.
$account = $this->drupalCreateUser(array('node test view'));
$this->drupalLogin($account);
// Ensure the node is published.
$this->assertTrue($node->isPublished(), 'Node is initially published.');
// Ensure that the node can not be edited.
$this->assertEqual(FALSE, $this->accessHandler->access($node, 'update', $node->prepareLangcode(), $account), 'The node may not be edited.');
// Test editing the node using the bulk form.
$edit = array(
'node_bulk_form[0]' => TRUE,
'action' => 'node_unpublish_action',
);
$this->drupalPostForm('test-node-bulk-form', $edit, t('Apply'));
$this->assertRaw(String::format('No access to execute %action on the @entity_type_label %entity_label.', [
'%action' => 'Unpublish content',
'@entity_type_label' => 'Content',
'%entity_label' => $node->label(),
]));
// Re-load the node and check the status.
$node = Node::load($node->id());
$this->assertTrue($node->isPublished(), 'The node is still published.');
// Create an account that may view the private node, but can update the
// status.
$account = $this->drupalCreateUser(array('administer nodes', 'edit any article content', 'node test view'));
$this->drupalLogin($account);
// Ensure the node is published.
$this->assertTrue($node->isPublished(), 'Node is initially published.');
// Ensure that the private node can not be edited.
$this->assertEqual(FALSE, $node->access('update', $account), 'The node may not be edited.');
$this->assertEqual(TRUE, $node->status->access('edit', $account), 'The node status can be edited.');
// Test editing the node using the bulk form.
$edit = array(
'node_bulk_form[0]' => TRUE,
'action' => 'node_unpublish_action',
);
$this->drupalPostForm('test-node-bulk-form', $edit, t('Apply'));
// Re-load the node and check the status.
$node = Node::load($node->id());
$this->assertTrue($node->isPublished(), 'The node is still published.');
}
/**
* Tests if nodes that may not be deleted, can not be deleted in bulk.
*/
public function testNodeDeleteAccess() {
// Create an account who will be the author of a private node.
$author = $this->drupalCreateUser();
// Create a private node (author may view, edit and delete, others may not).
$private_node = $this->drupalCreateNode(array(
'type' => 'article',
'private' => array(array(
'value' => TRUE,
)),
'uid' => $author->id(),
));
// Create an account that may view the private node, but not delete it.
$account = $this->drupalCreateUser(array('access content', 'administer nodes', 'delete own article content', 'node test view'));
// Create a node that may be deleted too, to ensure the delete confirmation
// page is shown later. In node_access_test.module, nodes may only be
// deleted by the author.
$own_node = $this->drupalCreateNode(array(
'type' => 'article',
'private' => array(array(
'value' => TRUE,
)),
'uid' => $account->id(),
));
$this->drupalLogin($account);
// Ensure that the private node can not be deleted.
$this->assertEqual(FALSE, $this->accessHandler->access($private_node, 'delete', $private_node->prepareLangcode(), $account), 'The private node may not be deleted.');
// Ensure that the public node may be deleted.
$this->assertEqual(TRUE, $this->accessHandler->access($own_node, 'delete', $own_node->prepareLangcode(), $account), 'The own node may be deleted.');
// Try to delete the node using the bulk form.
$edit = array(
'node_bulk_form[0]' => TRUE,
'node_bulk_form[1]' => TRUE,
'action' => 'node_delete_action',
);
$this->drupalPostForm('test-node-bulk-form', $edit, t('Apply'));
$this->drupalPostForm(NULL, array(), t('Delete'));
// Ensure the private node still exists.
$private_node = Node::load($private_node->id());
$this->assertNotNull($private_node, 'The private node has not been deleted.');
// Ensure the own node is deleted.
$own_node = Node::load($own_node->id());
$this->assertNull($own_node, 'The own node is deleted.');
}
}
......@@ -29,7 +29,7 @@ class BulkFormTest extends NodeTestBase {
*/
public function testBulkForm() {
$node_storage = $this->container->get('entity.manager')->getStorage('node');
$this->drupalLogin($this->drupalCreateUser(array('administer nodes')));
$this->drupalLogin($this->drupalCreateUser(array('administer nodes', 'access content overview', 'bypass node access')));
$node = $this->drupalCreateNode(array(
'promote' => FALSE,
));
......
......@@ -15,6 +15,7 @@
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Defines a actions-based bulk operation form element.
......@@ -33,7 +34,7 @@ class BulkForm extends FieldPluginBase {
/**
* An array of actions that can be executed.
*
* @var array
* @var \Drupal\system\ActionConfigEntityInterface[]
*/
protected $actions = array();
......@@ -249,18 +250,35 @@ protected function getBulkOptions($filtered = TRUE) {
* An associative array containing the structure of the form.