diff --git a/core/lib/Drupal/Core/Access/AccessGroupAnd.php b/core/lib/Drupal/Core/Access/AccessGroupAnd.php new file mode 100644 index 0000000000..64a66d7afb --- /dev/null +++ b/core/lib/Drupal/Core/Access/AccessGroupAnd.php @@ -0,0 +1,17 @@ +andIf($dependencyAccess); + } + +} diff --git a/core/lib/Drupal/Core/Access/AccessGroupOr.php b/core/lib/Drupal/Core/Access/AccessGroupOr.php new file mode 100644 index 0000000000..7389f2d318 --- /dev/null +++ b/core/lib/Drupal/Core/Access/AccessGroupOr.php @@ -0,0 +1,18 @@ +orIf($dependencyAccess); + } + +} diff --git a/core/lib/Drupal/Core/Access/AccessibleGroupBase.php b/core/lib/Drupal/Core/Access/AccessibleGroupBase.php new file mode 100644 index 0000000000..9f3076b792 --- /dev/null +++ b/core/lib/Drupal/Core/Access/AccessibleGroupBase.php @@ -0,0 +1,64 @@ +dependencies[] = $dependency; + return $this; + } + + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $access_result = NULL; + foreach ($this->dependencies as $dependency) { + $dependency_access_result = $dependency->access($operation, $account, TRUE); + if ($access_result === NULL) { + $access_result = $dependency_access_result; + } + else { + $access_result = $this->doCombineAccess($access_result, $dependency_access_result); + } + } + return $return_as_object ? $access_result : $access_result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function getDependencies() { + return $this->dependencies; + } + + /** + * Combines the access result of one dependency to previous dependencies. + * + * @param \Drupal\Core\Access\AccessResultInterface $accumulatedAccess + * The combine access result of previous dependencies. + * @param \Drupal\Core\Access\AccessResultInterface $dependencyAccess + * The access result of the current dependency. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The combined access result. + */ + abstract protected function doCombineAccess(AccessResultInterface $accumulatedAccess, AccessResultInterface $dependencyAccess); + +} diff --git a/core/lib/Drupal/Core/Access/AccessibleGroupInterface.php b/core/lib/Drupal/Core/Access/AccessibleGroupInterface.php new file mode 100644 index 0000000000..463eac3577 --- /dev/null +++ b/core/lib/Drupal/Core/Access/AccessibleGroupInterface.php @@ -0,0 +1,29 @@ +accessDependency; } + /** + * {@inheritdoc} + */ + public function mergeAccessDependency(AccessibleInterface $access_dependency) { + if (empty($this->accessDependency)) { + $this->accessDependency = $access_dependency; + return $this; + } + if (!$this->accessDependency instanceof AccessibleGroupInterface) { + $accessGroup = new AccessGroupAnd(); + $this->accessDependency = $accessGroup->addDependency($this->accessDependency); + } + $this->accessDependency->addDependency($access_dependency); + return $this; + } + } diff --git a/core/tests/Drupal/Tests/Core/Access/AccessGroupTest.php b/core/tests/Drupal/Tests/Core/Access/AccessGroupTest.php new file mode 100644 index 0000000000..e6086d1bbd --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/AccessGroupTest.php @@ -0,0 +1,66 @@ +account = $this->prophesize(AccountInterface::class)->reveal(); + } + + /** + * @covers \Drupal\Core\Access\AccessGroupAnd + * @covers \Drupal\Core\Access\AccessGroupOr + */ + public function testGroups() { + $allowedAccessible = $this->createAccessibleDouble(AccessResult::allowed()); + $forbiddenAccessible = $this->createAccessibleDouble(AccessResult::forbidden()); + $neutralAccessible = $this->createAccessibleDouble(AccessResult::neutral()); + + $orForbidden = new AccessGroupOr(); + $orForbidden->addDependency($allowedAccessible)->addDependency($forbiddenAccessible); + $this->assertTrue($orForbidden->access('view', $this->account, TRUE)->isForbidden()); + + $orAllowed = new AccessGroupOr(); + $orAllowed->addDependency($allowedAccessible)->addDependency($neutralAccessible); + $this->assertTrue($orAllowed->access('view', $this->account, TRUE)->isAllowed()); + + $andNeutral = new AccessGroupAnd(); + $andNeutral->addDependency($allowedAccessible)->addDependency($neutralAccessible); + $this->assertTrue($andNeutral->access('view', $this->account, TRUE)->isNeutral()); + + // We can also add groups and dependencies!!!!! Nested!!!!! + $andNeutral->addDependency($orAllowed); + $this->assertTrue($andNeutral->access('view', $this->account, TRUE)->isNeutral()); + + $andForbidden = $andNeutral; + $andForbidden->addDependency($forbiddenAccessible); + $this->assertTrue($andForbidden->access('view', $this->account, TRUE)->isForbidden()); + + // We can make groups from other groups! + $andGroupsForbidden = new AccessGroupAnd(); + $andGroupsForbidden->addDependency($andNeutral)->addDependency($andForbidden)->addDependency($orForbidden); + $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden()); + // But then would could also add a non-group accessible. + $andGroupsForbidden->addDependency($allowedAccessible); + $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden()); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php b/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php new file mode 100644 index 0000000000..e11a746a4d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php @@ -0,0 +1,36 @@ +prophesize(AccessibleInterface::class); + $accessible->access('view', $this->account, TRUE) + ->willReturn($accessResult); + return $accessible->reveal(); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php b/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php new file mode 100644 index 0000000000..86a98429d4 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php @@ -0,0 +1,162 @@ +account = $this->prophesize(AccountInterface::class)->reveal(); + $this->forbidden = $this->createAccessibleDouble(AccessResult::forbidden('Because I said so')); + $this->neutral = $this->createAccessibleDouble(AccessResult::neutral('I have no opinion')); + } + + /** + * Test that the previous dependency is replaced when using set. + * + * @covers ::setAccessDependency + * + * @dataProvider providerTestSetFirst + */ + public function testSetAccessDependency($use_set_first) { + $testRefinable = new RefinableDependentAccessTraitTestClass(); + + if ($use_set_first) { + $testRefinable->setAccessDependency($this->forbidden); + } + else { + $testRefinable->mergeAccessDependency($this->forbidden); + } + $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isForbidden()); + $this->assertEquals('Because I said so', $accessResult->getReason()); + + // Calling setAccessDependency() replaces the existing dependency. + $testRefinable->setAccessDependency($this->neutral); + $dependency = $testRefinable->getAccessDependency(); + $this->assertFalse($dependency instanceof AccessibleGroupInterface); + $accessResult = $dependency->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isNeutral()); + $this->assertEquals('I have no opinion', $accessResult->getReason()); + } + + /** + * Tests merging a new dependency with existing non-group access dependency. + * + * @dataProvider providerTestSetFirst + */ + public function testMergeNonGroup($use_set_first) { + $testRefinable = new RefinableDependentAccessTraitTestClass(); + if ($use_set_first) { + $testRefinable->setAccessDependency($this->forbidden); + } + else { + $testRefinable->mergeAccessDependency($this->forbidden); + } + + $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isForbidden()); + $this->assertEquals('Because I said so', $accessResult->getReason()); + + $testRefinable->mergeAccessDependency($this->neutral); + /** @var \Drupal\Core\Access\AccessGroupAnd $dependency */ + $dependency = $testRefinable->getAccessDependency(); + // Ensure the new dependency create a new AND group when merged. + $this->assertTrue($dependency instanceof AccessGroupAnd); + $dependencies = $dependency->getDependencies(); + $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultForbidden->isForbidden()); + $this->assertEquals('Because I said so', $accessResultForbidden->getReason()); + $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultNeutral->isNeutral()); + $this->assertEquals('I have no opinion', $accessResultNeutral->getReason()); + + } + + /** + * Tests merging a new dependency with an existing access group dependency. + * + * @dataProvider providerTestSetFirst + */ + public function testMergeGroup($use_set_first) { + $orGroup = new AccessGroupOr(); + $orGroup->addDependency($this->forbidden); + $testRefinable = new RefinableDependentAccessTraitTestClass(); + if ($use_set_first) { + $testRefinable->setAccessDependency($orGroup); + } + else { + $testRefinable->mergeAccessDependency($orGroup); + } + + $testRefinable->mergeAccessDependency($this->neutral); + /** @var \Drupal\Core\Access\AccessGroupOr $dependency */ + $dependency = $testRefinable->getAccessDependency(); + + // Ensure the new dependency is merged with the existing group. + $this->assertTrue($dependency instanceof AccessGroupOr); + $dependencies = $dependency->getDependencies(); + $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultForbidden->isForbidden()); + $this->assertEquals('Because I said so', $accessResultForbidden->getReason()); + $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultNeutral->isNeutral()); + $this->assertEquals('I have no opinion', $accessResultNeutral->getReason()); + } + + /** + * Dataprovider for all test methods. + * + * Provides test cases for calling setAccessDependency() or + * mergeAccessDependency() first. A call to either should behave the same on a + * new RefinableDependentAccessInterface object. + */ + public function providerTestSetFirst() { + return [ + [TRUE], + [FALSE], + ]; + } + +} + +/** + * Test class that implements RefinableDependentAccessInterface. + */ +class RefinableDependentAccessTraitTestClass implements RefinableDependentAccessInterface { + + use RefinableDependentAccessTrait; + +}