Skip to content

Commit

Permalink
Issue #2593139: Add configure_permission annotation key for plugins.
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob Thorne authored and Rob Thorne committed Nov 26, 2015
1 parent 93f177b commit 0ee695b
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 7 deletions.
9 changes: 7 additions & 2 deletions src/Context/AnnotatedClassDiscovery.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

/**
* Extends the annotation class discovery for usage with Rules context.
*
* We modify the annotations classes for ContextDefinition and for Condition.
* This class makes sure that our plugin managers apply these.
*
*/
class AnnotatedClassDiscovery extends CoreAnnotatedClassDiscovery {

Expand All @@ -24,9 +28,10 @@ protected function getAnnotationReader() {
// reader on our own, so we can control the order of namespaces.
$this->annotationReader = new SimpleAnnotationReader();

// Make sure to add our namespace first, so our ContextDefinition class
// gets picked.
// Make sure to add our namespace first, so our ContextDefinition and
// Condition annotations gets picked.
$this->annotationReader->addNamespace('Drupal\rules\Context\Annotation');
$this->annotationReader->addNamespace('Drupal\rules\Core\Annotation');
// Add the namespaces from the main plugin annotation, like @EntityType.
$namespace = substr($this->pluginDefinitionAnnotationName, 0, strrpos($this->pluginDefinitionAnnotationName, '\\'));
$this->annotationReader->addNamespace($namespace);
Expand Down
49 changes: 49 additions & 0 deletions src/Core/Annotation/Condition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
/**
* @file
* Contains Drupal\rules\Core\Annotation\Condition.
*/

namespace Drupal\rules\Core\Annotation;

use Drupal\Core\Condition\Annotation\Condition as CoreConditionAnnotation;

/**
* Extension of the Condition annotation class.
*
* @Annotation
*
* This class adds a configuration access parameter to the Condition
* annotation.
*/
class Condition extends CoreConditionAnnotation {

/**
* The permissions allowed to access the configuration UI for this plugin.
*
* @var string[]
* Array of permission strings as declared in a *.permissions.yml file. If
* any one of these permissions apply for the relevant user, we allow access.
*
* The key should be used as follows. Note that we add a space between "@"
* and "Condition", since we do not want to trigger the annotation parser
* here; you should remove that space in your actual annotation:
*
* @ Condition(
* id = "my_module_user_is_blocked",
* label = @Translation("My User is blocked"),
* category = @Translation("User"),
* context = {
* "user" = @ContextDefinition("entity:user",
* label = @Translation("User")
* ),
* configure_permissions = {
* "administer users",
* "block users"
* }
* }
* )
*/
public $configure_permissions;

}
8 changes: 8 additions & 0 deletions src/Core/Annotation/RulesAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ class RulesAction extends Plugin {
*/
public $category;

/**
* The permission required to access the configuration UI for this plugin.
*
* @var string
* Permission string as declared in a *.permissions.yml file.
*/
public $configuration_access;

/**
* Defines the used context of the action plugin.
*
Expand Down
36 changes: 36 additions & 0 deletions src/Core/ConfigurationAccessControlInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\rules\Core\ConfigurationAccessControlInterface.
*/

namespace Drupal\rules\Core;

use Drupal\Core\Session\AccountInterface;

/**
* Defines a configuration permission control interface.
*
* @see \Drupal\rules\Core\ConfigurationAccessControlTrait.
*/
interface ConfigurationAccessControlInterface {

/**
* Check configuration access.
*
* @param 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 checkConfigurationAccess(AccountInterface $account = NULL, $return_as_object = FALSE);

}
73 changes: 73 additions & 0 deletions src/Core/ConfigurationAccessControlTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/**
* @file
* Contains \Drupal\rules\Core\ConfigurationAccessControlTrait.
*/

namespace Drupal\rules\Core;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;

/**
* Implements access related functions for plugins.
*/
trait ConfigurationAccessControlTrait {

/**
* Checks configuration permission.
*
* @param 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 checkConfigurationAccess(AccountInterface $account = NULL, $return_as_object = FALSE) {
if (!$account) {
$account = \Drupal::currentUser();
}
// We treat these as our "super-user" accesses. We let the reaction
// rule and component permissions control the main admin UI.
$admin_perms = [
'administer rules',
'bypass rules access',
];

$access = FALSE;
foreach ($admin_perms as $perm) {
if ($account->hasPermission($perm)) {
$access = TRUE;
break;
}
}

if (!$access) {
// See if the plugin has a configuration_access annotation.
$definition = $this->getPluginDefinition();
if (!empty($definition['configure_permissions']) && is_array($definition['configure_permissions'])) {
foreach ($definition['configure_permissions'] as $perm) {
if ($account->hasPermission($perm)) {
$access = TRUE;
break;
}
}
}
}

if ($return_as_object) {
// Arguably, we should return AccessResult::neutral() here instead.
return $access ? AccessResult::allowed() : FALSE;
}
return $access;
}

}
6 changes: 4 additions & 2 deletions src/Core/RulesActionBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@

namespace Drupal\rules\Core;

use Drupal\Core\Access\AccessResultForbidden;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Plugin\ContextAwarePluginBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\rules\Context\ContextProviderTrait;
use Drupal\rules\Core\ConfigurationAccessControlTrait;

/**
* Base class for rules actions.
Expand All @@ -19,6 +20,7 @@ abstract class RulesActionBase extends ContextAwarePluginBase implements RulesAc

use ContextProviderTrait;
use ExecutablePluginTrait;
use ConfigurationAccessControlTrait;

/**
* The plugin configuration.
Expand Down Expand Up @@ -86,7 +88,7 @@ public function autoSaveContext() {
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
// Just deny access per default for now.
if ($return_as_object) {
return new AccessResultForbidden();
return AccessResult::forbidden();
}
return FALSE;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Core/RulesActionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
use Drupal\Core\Session\AccountInterface;
use Drupal\rules\Context\ContextAwarePluginInterface;
use Drupal\rules\Context\ContextProviderInterface;
use Drupal\rules\Core\ConfigurationAccessControlInterface;

/**
* Extends the core ActionInterface to provide context.
*/
interface RulesActionInterface extends ExecutableInterface, ContextAwarePluginInterface, ContextProviderInterface {
interface RulesActionInterface extends ExecutableInterface, ContextAwarePluginInterface, ContextProviderInterface, ConfigurationAccessControlInterface {

/**
* Returns a list of context names that should be auto-saved after execution.
Expand Down
2 changes: 2 additions & 0 deletions src/Core/RulesConditionBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\rules\Context\ContextProviderTrait;
use Drupal\rules\Core\ConfigurationAccessControlTrait;

/**
* Base class for rules conditions.
Expand All @@ -19,6 +20,7 @@ abstract class RulesConditionBase extends ConditionPluginBase implements RulesCo

use ContextProviderTrait;
use ExecutablePluginTrait;
use ConfigurationAccessControlTrait;

/**
* {@inheritdoc}
Expand Down
4 changes: 3 additions & 1 deletion src/Core/RulesConditionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
use Drupal\rules\Context\ContextAwarePluginInterface;
use Drupal\Core\Condition\ConditionInterface;
use Drupal\rules\Context\ContextProviderInterface;
use Drupal\rules\Core\ConfigurationAccessControlInterface;


/**
* Extends the core ConditionInterface to provide a negate() method.
*/
interface RulesConditionInterface extends ConditionInterface, ContextAwarePluginInterface, ContextProviderInterface {
interface RulesConditionInterface extends ConditionInterface, ContextAwarePluginInterface, ContextProviderInterface, ConfigurationAccessControlInterface {

/**
* Negates the result after evaluating this condition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
* "text" = @ContextDefinition("string",
* label = @Translation("Text to compare")
* )
* }
* },
* configure_permissions = { "access test configuration" }
* )
*/
class TestTextCondition extends RulesConditionBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* label = @Translation("Text to concatenate")
* )
* },
* configure_permissions = { "access test configuration" },
* provides = {
* "concatenated" = @ContextDefinition("string",
* label = @Translation("Concatenated result")
Expand Down
58 changes: 58 additions & 0 deletions tests/src/Integration/Action/RulesActionAccessTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
/**
* @file
* Contains Drupal\Tests\rules\Integration\Action\RulesActionAccessTest.
*/

namespace Drupal\Tests\rules\Integration\Action;

use Drupal\Tests\rules\Integration\RulesIntegrationTestBase;
use Drupal\Core\Session\AccountInterface;
use Prophecy\Argument;

/**
* Tests configuration access control for Rules Actions.
*
* @group rules_actions
*/
class RulesActionAccessTest extends RulesIntegrationTestBase {

/**
* Confirm that a condition plugin respects configure permission.
*/
public function testHasConfigurationAccessInfo() {
$plugin = $this->actionManager->createInstance('rules_test_string');
$this->assertNotNull($plugin, "The rules_test action was found.");
$definition = $plugin->getPluginDefinition();
$this->assertNotEmpty($definition['configure_permissions'], "Plugin has configuration access info.");
$perms = $definition['configure_permissions'];
$this->assertTrue(is_array($perms), "configure_permissions is an array");
$this->assertContains("access test configuration", $perms, "Expected permission found in configure_permissions.");

// Now see if the permission is actually used.
$user_with_perm = $this->prophesize(AccountInterface::class);
$user_with_perm
->hasPermission("access test configuration")
->willReturn(TRUE)
->shouldBeCalledTimes(1);
$user_with_perm
->hasPermission(Argument::type('string'))
->willReturn(FALSE);

$this->container->set('current_user', $user_with_perm->reveal());
$this->assertTrue($plugin->checkConfigurationAccess(), "User with permission has configuration access.");

$user_without_perm = $this->prophesize(AccountInterface::class);
$user_without_perm
->hasPermission("access test configuration")
->willReturn(FALSE)
->shouldBeCalledTimes(1);
$user_without_perm
->hasPermission(Argument::type('string'))
->willReturn(FALSE);

$this->assertFalse($plugin->checkConfigurationAccess($user_without_perm->reveal()),
"User without permission does not have configuration access.");
}

}

0 comments on commit 0ee695b

Please sign in to comment.