Skip to content
Snippets Groups Projects
Commit e749f72b authored by Kristiaan Van den Eynde's avatar Kristiaan Van den Eynde
Browse files

Issue #3319906 by kristiaanvandeneynde: Make it possible to alter calculated permissions

parent 7376bc3c
No related branches found
Tags 1.0.0-alpha19
No related merge requests found
......@@ -18,9 +18,7 @@ trait CalculatedPermissionsTrait {
* {@inheritdoc}
*/
public function getItem($scope, $identifier) {
return isset($this->items[$scope][$identifier])
? $this->items[$scope][$identifier]
: FALSE;
return $this->items[$scope][$identifier] ?? FALSE;
}
/**
......
......@@ -132,6 +132,7 @@ class ChainPermissionCalculator implements ChainPermissionCalculatorInterface {
}
// Otherwise build the permissions and store them in the persistent cache.
else {
// Build mode, allow all calculators to add initial data.
$calculated_permissions = new RefinableCalculatedPermissions();
foreach ($this->getCalculators() as $calculator) {
$calculator_permissions = $calculator->calculatePermissions($account, $scope);
......@@ -147,6 +148,14 @@ class ChainPermissionCalculator implements ChainPermissionCalculatorInterface {
$calculated_permissions = $calculated_permissions->merge($calculator_permissions);
}
// Alter mode, allow all calculators to alter the complete build.
$calculated_permissions->disableBuildMode();
foreach ($this->getCalculators() as $calculator) {
if ($calculator instanceof PermissionCalculatorAlterInterface) {
$calculator->alterPermissions($calculated_permissions);
}
}
// Apply a cache tag to easily flush the calculated permissions.
$calculated_permissions->addCacheTags(['flexible_permissions']);
}
......
......@@ -7,8 +7,6 @@ namespace Drupal\flexible_permissions;
*
* Each calculator in the chain can be another chain, which is why this
* interface extends the permission calculator one.
*
* @todo Add alterPermissions($permissions, $account, $scope)?
*/
interface ChainPermissionCalculatorInterface extends PermissionCalculatorInterface {
......
<?php
namespace Drupal\flexible_permissions;
/**
* Defines an interface to alter the final calculated permissions.
*/
interface PermissionCalculatorAlterInterface {
/**
* Alter the permissions after all calculators have finished building them.
*
* @param \Drupal\flexible_permissions\RefinableCalculatedPermissionsInterface $calculated_permissions
* The completely built calculated permissions.
*/
public function alterPermissions(RefinableCalculatedPermissionsInterface $calculated_permissions);
}
......@@ -7,7 +7,7 @@ use Drupal\Core\Session\AccountInterface;
/**
* Base class for permission calculators.
*/
abstract class PermissionCalculatorBase implements PermissionCalculatorInterface {
abstract class PermissionCalculatorBase implements PermissionCalculatorAlterInterface, PermissionCalculatorInterface {
/**
* {@inheritdoc}
......@@ -16,6 +16,11 @@ abstract class PermissionCalculatorBase implements PermissionCalculatorInterface
return (new RefinableCalculatedPermissions())->addCacheContexts($this->getPersistentCacheContexts($scope));
}
/**
* {@inheritdoc}
*/
public function alterPermissions(RefinableCalculatedPermissionsInterface $calculated_permissions) {}
/**
* {@inheritdoc}
*/
......
......@@ -14,10 +14,26 @@ class RefinableCalculatedPermissions implements RefinableCalculatedPermissionsIn
use CalculatedPermissionsTrait;
use RefinableCacheableDependencyTrait;
/**
* Whether the object is currently building permissions.
*
* @var bool
*/
protected $buildMode = TRUE;
/**
* {@inheritdoc}
*/
public function disableBuildMode() {
$this->buildMode = FALSE;
}
/**
* {@inheritdoc}
*/
public function addItem(CalculatedPermissionsItemInterface $item, $overwrite = FALSE) {
// Only allow overwriting when not in build mode.
$overwrite = $overwrite && !$this->buildMode;
if (!$overwrite && $existing = $this->getItem($item->getScope(), $item->getIdentifier())) {
$item = $this->mergeItems($existing, $item);
}
......@@ -29,7 +45,9 @@ class RefinableCalculatedPermissions implements RefinableCalculatedPermissionsIn
* {@inheritdoc}
*/
public function removeItem($scope, $identifier) {
unset($this->items[$scope][$identifier]);
if (!$this->buildMode) {
unset($this->items[$scope][$identifier]);
}
return $this;
}
......@@ -37,7 +55,9 @@ class RefinableCalculatedPermissions implements RefinableCalculatedPermissionsIn
* {@inheritdoc}
*/
public function removeItems() {
$this->items = [];
if (!$this->buildMode) {
$this->items = [];
}
return $this;
}
......@@ -45,7 +65,9 @@ class RefinableCalculatedPermissions implements RefinableCalculatedPermissionsIn
* {@inheritdoc}
*/
public function removeItemsByScope($scope) {
unset($this->items[$scope]);
if (!$this->buildMode) {
unset($this->items[$scope]);
}
return $this;
}
......
......@@ -9,6 +9,16 @@ use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
*/
interface RefinableCalculatedPermissionsInterface extends RefinableCacheableDependencyInterface, CalculatedPermissionsInterface {
/**
* Disables build mode.
*
* When build mode is on, which is the default state, only new items can be
* added. Only after build mode is disabled can items be removed or replaced.
*
* @internal
*/
public function disableBuildMode();
/**
* Adds a calculated permission item.
*
......@@ -17,7 +27,7 @@ interface RefinableCalculatedPermissionsInterface extends RefinableCacheableDepe
* @param bool $overwrite
* (optional) Whether to overwrite an item if there already is one for the
* given identifier within the scope. Defaults to FALSE, meaning a merge
* will take place instead.
* will take place instead. Does nothing if build mode is still enabled.
*
* @return $this
*/
......@@ -26,6 +36,8 @@ interface RefinableCalculatedPermissionsInterface extends RefinableCacheableDepe
/**
* Removes a single calculated permission item from a given scope.
*
* Does nothing if build mode is still enabled.
*
* @param $scope
* The scope name to remove the item from.
* @param $identifier
......@@ -38,6 +50,8 @@ interface RefinableCalculatedPermissionsInterface extends RefinableCacheableDepe
/**
* Removes all of the calculated permission items, regardless of scope.
*
* Does nothing if build mode is still enabled.
*
* @return $this
*/
public function removeItems();
......@@ -45,6 +59,8 @@ interface RefinableCalculatedPermissionsInterface extends RefinableCacheableDepe
/**
* Removes all of the calculated permission items for the given scope.
*
* Does nothing if build mode is still enabled.
*
* @param string $scope
* The scope name to remove the items for.
*
......
......@@ -12,6 +12,7 @@ use Drupal\flexible_permissions\CalculatedPermissionsScopeException;
use Drupal\flexible_permissions\ChainPermissionCalculator;
use Drupal\flexible_permissions\PermissionCalculatorBase;
use Drupal\flexible_permissions\RefinableCalculatedPermissions;
use Drupal\flexible_permissions\RefinableCalculatedPermissionsInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\variationcache\Cache\VariationCacheInterface;
use Prophecy\Argument;
......@@ -47,8 +48,13 @@ class ChainPermissionCalculatorTest extends UnitTestCase {
*/
public function testAddCalculator() {
$chain_calculator = $this->setUpChainCalculator();
$calculators = [
new FooScopeCalculator(),
new BarScopeCalculator(),
new BarAlterCalculator(),
];
foreach ($calculators = [new BarScopeCalculator(), new FooScopeCalculator()] as $calculator) {
foreach ($calculators as $calculator) {
$chain_calculator->addCalculator($calculator);
}
......@@ -62,15 +68,12 @@ class ChainPermissionCalculatorTest extends UnitTestCase {
*/
public function testGetPersistentCacheContexts() {
$chain_calculator = $this->setUpChainCalculator();
$persistent_cache_contexts = [];
foreach ([new FooScopeCalculator(), new BarScopeCalculator()] as $calculator) {
$persistent_cache_contexts = array_merge($persistent_cache_contexts, $calculator->getPersistentCacheContexts('baz'));
foreach ([new FooScopeCalculator(), new BarScopeCalculator(), new BarAlterCalculator()] as $calculator) {
$chain_calculator->addCalculator($calculator);
}
$calculator_contexts = $chain_calculator->getPersistentCacheContexts('baz');
$this->assertEquals($persistent_cache_contexts, $calculator_contexts, 'Cache contexts match!');
$this->assertEquals(['foo', 'bar', 'baz'], $chain_calculator->getPersistentCacheContexts('anything'), 'Cache contexts match!');
}
/**
......@@ -87,8 +90,27 @@ class ChainPermissionCalculatorTest extends UnitTestCase {
$calculator_permissions = $calculator->calculatePermissions($account, 'bar');
$calculator_permissions->addCacheTags(['flexible_permissions']);
$calculated_permissions = $chain_calculator->calculatePermissions($account, 'bar');
$this->assertEquals(new CalculatedPermissions($calculator_permissions), $calculated_permissions);
$this->assertEquals(new CalculatedPermissions($calculator_permissions), $chain_calculator->calculatePermissions($account, 'bar'));
}
/**
* Tests that calculators can alter the final result.
*
* @covers ::calculatePermissions
*/
public function testAlterPermissions() {
$account = $this->prophesize(AccountInterface::class)->reveal();
$chain_calculator = $this->setUpChainCalculator();
$chain_calculator->addCalculator(new BarScopeCalculator());
$chain_calculator->addCalculator(new BarAlterCalculator());
$actual_permissions = $chain_calculator
->calculatePermissions($account, 'bar')
->getItem('bar', 1)
->getPermissions();
$this->assertEquals(['foo', 'baz'], $actual_permissions);
}
/**
......@@ -209,6 +231,7 @@ class ChainPermissionCalculatorTest extends UnitTestCase {
$bar_calculator = new BarScopeCalculator();
$bar_permissions = $bar_calculator->calculatePermissions($account, $scope);
$bar_permissions->addCacheTags(['flexible_permissions']);
$bar_permissions->disableBuildMode();
$none_refinable_bar_permissions = new CalculatedPermissions($bar_permissions);
$cache_static = $this->prophesize(VariationCacheInterface::class);
......@@ -305,7 +328,7 @@ class FooScopeCalculator extends PermissionCalculatorBase {
public function calculatePermissions(AccountInterface $account, $scope) {
$calculated_permissions = parent::calculatePermissions($account, $scope);
return $calculated_permissions->addItem(new CalculatedPermissionsItem('foo', 1, [], TRUE));
return $calculated_permissions->addItem(new CalculatedPermissionsItem('foo', 1, ['foo', 'bar'], TRUE));
}
public function getPersistentCacheContexts($scope) {
......@@ -318,7 +341,7 @@ class BarScopeCalculator extends PermissionCalculatorBase {
public function calculatePermissions(AccountInterface $account, $scope) {
$calculated_permissions = parent::calculatePermissions($account, $scope);
return $calculated_permissions->addItem(new CalculatedPermissionsItem('bar', 1, [], TRUE));
return $calculated_permissions->addItem(new CalculatedPermissionsItem('bar', 1, ['foo', 'bar']));
}
public function getPersistentCacheContexts($scope) {
......@@ -327,6 +350,34 @@ class BarScopeCalculator extends PermissionCalculatorBase {
}
class BarAlterCalculator extends PermissionCalculatorBase {
public function alterPermissions(RefinableCalculatedPermissionsInterface $calculated_permissions) {
parent::alterPermissions($calculated_permissions);
foreach ($calculated_permissions->getItemsByScope('bar') as $item) {
$permissions = $item->getPermissions();
if (($key = array_search('bar', $permissions, TRUE)) !== FALSE) {
$permissions[$key] = 'baz';
$new_item = new CalculatedPermissionsItem(
$item->getScope(),
$item->getIdentifier(),
$permissions
);
$calculated_permissions->addItem($new_item, TRUE);
}
}
}
public function getPersistentCacheContexts($scope) {
return ['baz'];
}
}
class EmptyCalculator extends PermissionCalculatorBase {
}
......
......@@ -35,15 +35,36 @@ class RefinableCalculatedPermissionsTest extends UnitTestCase {
$calculated_permissions->addItem($item);
$this->assertEquals(['bar', 'baz'], $calculated_permissions->getItem($scope, 'foo')->getPermissions(), 'Adding a calculated permissions item that was already in the list merges them.');
$calculated_permissions->addItem($item, TRUE);
$this->assertEquals(['baz'], $calculated_permissions->getItem($scope, 'foo')->getPermissions(), 'Successfully overwrote an item that was already in the list.');
$item = new CalculatedPermissionsItem($scope, 'foo', ['cat'], TRUE);
$calculated_permissions->addItem($item);
$this->assertEquals([], $calculated_permissions->getItem($scope, 'foo')->getPermissions(), 'Merging in a calculated permissions item with admin rights empties the permissions.');
$this->assertTrue($calculated_permissions->getItem($scope, 'foo')->isAdmin(), 'Merging in a calculated permissions item with admin rights flags the result as having admin rights.');
}
/**
* Tests the overwriting of a calculated permissions item.
*
* @covers ::addItem
* @covers ::getItem
* @depends testAddItem
*/
public function testAddItemOverwrite() {
$calculated_permissions = new RefinableCalculatedPermissions();
$scope = 'some_scope';
$item = new CalculatedPermissionsItem($scope, 'foo', ['bar']);
$calculated_permissions->addItem($item);
$item = new CalculatedPermissionsItem($scope, 'foo', ['baz']);
$calculated_permissions->addItem($item, TRUE);
$this->assertEquals(['bar', 'baz'], $calculated_permissions->getItem($scope, 'foo')->getPermissions(), 'Could not overwrite item in build mode.');
$calculated_permissions->disableBuildMode();
$calculated_permissions->addItem($item, TRUE);
$this->assertEquals(['baz'], $calculated_permissions->getItem($scope, 'foo')->getPermissions(), 'Successfully overwrote an item that was already in the list.');
}
/**
* Tests the removal of a calculated permissions item.
*
......@@ -55,10 +76,13 @@ class RefinableCalculatedPermissionsTest extends UnitTestCase {
$item = new CalculatedPermissionsItem($scope, 'foo', ['bar']);
$calculated_permissions = new RefinableCalculatedPermissions();
$calculated_permissions
->addItem($item)
->removeItem($scope, 'foo');
$calculated_permissions->addItem($item);
$calculated_permissions->removeItem($scope, 'foo');
$this->assertNotFalse($calculated_permissions->getItem($scope, 'foo'), 'Could not remove item in build mode.');
$calculated_permissions->disableBuildMode();
$calculated_permissions->removeItem($scope, 'foo');
$this->assertFalse($calculated_permissions->getItem($scope, 'foo'), 'Could not retrieve a removed item.');
}
......@@ -73,10 +97,13 @@ class RefinableCalculatedPermissionsTest extends UnitTestCase {
$item = new CalculatedPermissionsItem($scope, 'foo', ['bar']);
$calculated_permissions = new RefinableCalculatedPermissions();
$calculated_permissions
->addItem($item)
->removeItems();
$calculated_permissions->addItem($item);
$calculated_permissions->removeItems();
$this->assertNotFalse($calculated_permissions->getItem($scope, 'foo'), 'Could not remove items in build mode.');
$calculated_permissions->disableBuildMode();
$calculated_permissions->removeItems();
$this->assertFalse($calculated_permissions->getItem($scope, 'foo'), 'Could not retrieve a removed item.');
}
......@@ -98,7 +125,10 @@ class RefinableCalculatedPermissionsTest extends UnitTestCase {
->addItem($item_a)
->addItem($item_b)
->removeItemsByScope($scope_a);
$this->assertNotFalse($calculated_permissions->getItem($scope_a, 'foo'), 'Could not remove items in build mode.');
$calculated_permissions->disableBuildMode();
$calculated_permissions->removeItemsByScope($scope_a);
$this->assertFalse($calculated_permissions->getItem($scope_a, 'foo'), 'Could not retrieve a removed item.');
$this->assertNotFalse($calculated_permissions->getItem($scope_b, 1), 'Untouched scope item was found.');
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment