Loading core/lib/Drupal/Core/Session/AccessPolicyProcessor.php +37 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\VariationCacheInterface; use Drupal\Core\Utility\FiberResumeType; /** * Processes access policies into permissions for an account. Loading Loading @@ -39,6 +40,42 @@ public function addAccessPolicy(AccessPolicyInterface $access_policy): void { * {@inheritdoc} */ public function processAccessPolicies(AccountInterface $account, string $scope = AccessPolicyInterface::SCOPE_DRUPAL): CalculatedPermissionsInterface { if (!\Fiber::getCurrent()) { return $this->doProcessAccessPolicies($account, $scope); } // If running in a fiber, prevent the current user switch from escaping to // outside the fiber by resuming the fiber if it was suspended. $fiber = new \Fiber([$this, 'doProcessAccessPolicies']); $fiber->start($account, $scope); while (!$fiber->isTerminated()) { if ($fiber->isSuspended()) { $resume_type = $fiber->resume(); if (!$fiber->isTerminated() && $resume_type !== FiberResumeType::Immediate) { usleep(500); } } } return $fiber->getReturn(); } /** * Processes the access policies for an account within a given scope. * * @param \Drupal\Core\Session\AccountInterface $account * The user account for which to calculate the permissions. * @param string $scope * The scope to calculate the permissions. * * @return \Drupal\Core\Session\CalculatedPermissionsInterface * The access policies' permissions within the given scope. * * @throws \Drupal\Core\Session\AccessPolicyScopeException * Thrown if an access policy returns permissions for a scope other than the * one passed in. */ public function doProcessAccessPolicies(AccountInterface $account, string $scope): CalculatedPermissionsInterface { $persistent_cache_contexts = $this->getPersistentCacheContexts($scope); $initial_cacheability = (new CacheableMetadata())->addCacheContexts($persistent_cache_contexts); $cache_keys = ['access_policies', $scope]; Loading core/tests/Drupal/KernelTests/Core/Session/AccessPolicyProcessorInFibersTest.php 0 → 100644 +106 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\KernelTests\Core\Session; use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Security\Attribute\TrustedCallback; use Drupal\Core\Session\AccessPolicyProcessor; use Drupal\Core\Session\UserSession; use Drupal\KernelTests\KernelTestBase; use Drupal\Tests\user\Traits\UserCreationTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the behavior of the access policy processor running inside fibers. */ #[CoversClass(AccessPolicyProcessor::class)] #[Group('Session')] #[RunTestsInSeparateProcesses] class AccessPolicyProcessorInFibersTest extends KernelTestBase { use UserCreationTrait; /** * {@inheritdoc} */ protected static $modules = ['system', 'user']; /** * Tests the behavior of the access policy processor running inside fibers. */ public function testAccessPolicyProcessorInFibers(): void { // Create a role and then empty the static cache, so that it will need to be // loaded from storage. $this->createRole(['administer modules'], 'test_role'); \Drupal::entityTypeManager()->getStorage('user_role')->resetCache(); // Create a render array with two elements that have lazy builders. The // first lazy builder prints the ID of the current user. The second lazy // builder checks the permissions of a different user, which results in a // call to AccountPolicyProcessor::processAccessPolicies(). In that method, // if the current user ID is different from the ID of the account being // processed, the current user is temporarily switched to that account. // This is done in order to make sure the correct user's data is used when // saving to the variation cache. // // Note that for the purposes of this test, the lazy builder that accesses // the current user ID has to come before the other lazy builder in the // render array. Ordering the array this way results in the second lazy // builder starting to run before the first. This happens because as render // contexts are updated as they are bubbled, the BubbleableMetadata object // associated with the render element merges its attached placeholder to the // front of the list to be processed. $build = [ [ '#lazy_builder' => [self::class . '::lazyBuilderCheckCurrentUserCallback', []], '#create_placeholder' => TRUE, ], [ // Add a space between placeholders. '#markup' => ' ', ], [ '#lazy_builder' => [self::class . '::lazyBuilderCheckAccessCallback', []], '#create_placeholder' => TRUE, ], ]; $user2 = new UserSession(['uid' => 2]); $this->setCurrentUser($user2); $expected = 'The current user id is 2. User 3 can administer modules.'; $output = (string) \Drupal::service(RendererInterface::class)->renderRoot($build); $this->assertSame($expected, $output); } /** * Lazy builder that displays the current user ID. */ #[TrustedCallback] public static function lazyBuilderCheckCurrentUserCallback(): array { return [ '#markup' => new FormattableMarkup('The current user id is @id.', ['@id' => \Drupal::currentUser()->id()]), ]; } /** * Lazy builder that checks permissions on a different user. */ #[TrustedCallback] public static function lazyBuilderCheckAccessCallback(): array { $user3 = new UserSession([ 'uid' => 3, 'roles' => ['test_role' => 'test_role'], ]); return [ '#markup' => new FormattableMarkup('User @id can administer modules.', ['@id' => $user3->id()]), '#access' => $user3->hasPermission('administer modules'), ]; } } Loading
core/lib/Drupal/Core/Session/AccessPolicyProcessor.php +37 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\VariationCacheInterface; use Drupal\Core\Utility\FiberResumeType; /** * Processes access policies into permissions for an account. Loading Loading @@ -39,6 +40,42 @@ public function addAccessPolicy(AccessPolicyInterface $access_policy): void { * {@inheritdoc} */ public function processAccessPolicies(AccountInterface $account, string $scope = AccessPolicyInterface::SCOPE_DRUPAL): CalculatedPermissionsInterface { if (!\Fiber::getCurrent()) { return $this->doProcessAccessPolicies($account, $scope); } // If running in a fiber, prevent the current user switch from escaping to // outside the fiber by resuming the fiber if it was suspended. $fiber = new \Fiber([$this, 'doProcessAccessPolicies']); $fiber->start($account, $scope); while (!$fiber->isTerminated()) { if ($fiber->isSuspended()) { $resume_type = $fiber->resume(); if (!$fiber->isTerminated() && $resume_type !== FiberResumeType::Immediate) { usleep(500); } } } return $fiber->getReturn(); } /** * Processes the access policies for an account within a given scope. * * @param \Drupal\Core\Session\AccountInterface $account * The user account for which to calculate the permissions. * @param string $scope * The scope to calculate the permissions. * * @return \Drupal\Core\Session\CalculatedPermissionsInterface * The access policies' permissions within the given scope. * * @throws \Drupal\Core\Session\AccessPolicyScopeException * Thrown if an access policy returns permissions for a scope other than the * one passed in. */ public function doProcessAccessPolicies(AccountInterface $account, string $scope): CalculatedPermissionsInterface { $persistent_cache_contexts = $this->getPersistentCacheContexts($scope); $initial_cacheability = (new CacheableMetadata())->addCacheContexts($persistent_cache_contexts); $cache_keys = ['access_policies', $scope]; Loading
core/tests/Drupal/KernelTests/Core/Session/AccessPolicyProcessorInFibersTest.php 0 → 100644 +106 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\KernelTests\Core\Session; use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Security\Attribute\TrustedCallback; use Drupal\Core\Session\AccessPolicyProcessor; use Drupal\Core\Session\UserSession; use Drupal\KernelTests\KernelTestBase; use Drupal\Tests\user\Traits\UserCreationTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; /** * Tests the behavior of the access policy processor running inside fibers. */ #[CoversClass(AccessPolicyProcessor::class)] #[Group('Session')] #[RunTestsInSeparateProcesses] class AccessPolicyProcessorInFibersTest extends KernelTestBase { use UserCreationTrait; /** * {@inheritdoc} */ protected static $modules = ['system', 'user']; /** * Tests the behavior of the access policy processor running inside fibers. */ public function testAccessPolicyProcessorInFibers(): void { // Create a role and then empty the static cache, so that it will need to be // loaded from storage. $this->createRole(['administer modules'], 'test_role'); \Drupal::entityTypeManager()->getStorage('user_role')->resetCache(); // Create a render array with two elements that have lazy builders. The // first lazy builder prints the ID of the current user. The second lazy // builder checks the permissions of a different user, which results in a // call to AccountPolicyProcessor::processAccessPolicies(). In that method, // if the current user ID is different from the ID of the account being // processed, the current user is temporarily switched to that account. // This is done in order to make sure the correct user's data is used when // saving to the variation cache. // // Note that for the purposes of this test, the lazy builder that accesses // the current user ID has to come before the other lazy builder in the // render array. Ordering the array this way results in the second lazy // builder starting to run before the first. This happens because as render // contexts are updated as they are bubbled, the BubbleableMetadata object // associated with the render element merges its attached placeholder to the // front of the list to be processed. $build = [ [ '#lazy_builder' => [self::class . '::lazyBuilderCheckCurrentUserCallback', []], '#create_placeholder' => TRUE, ], [ // Add a space between placeholders. '#markup' => ' ', ], [ '#lazy_builder' => [self::class . '::lazyBuilderCheckAccessCallback', []], '#create_placeholder' => TRUE, ], ]; $user2 = new UserSession(['uid' => 2]); $this->setCurrentUser($user2); $expected = 'The current user id is 2. User 3 can administer modules.'; $output = (string) \Drupal::service(RendererInterface::class)->renderRoot($build); $this->assertSame($expected, $output); } /** * Lazy builder that displays the current user ID. */ #[TrustedCallback] public static function lazyBuilderCheckCurrentUserCallback(): array { return [ '#markup' => new FormattableMarkup('The current user id is @id.', ['@id' => \Drupal::currentUser()->id()]), ]; } /** * Lazy builder that checks permissions on a different user. */ #[TrustedCallback] public static function lazyBuilderCheckAccessCallback(): array { $user3 = new UserSession([ 'uid' => 3, 'roles' => ['test_role' => 'test_role'], ]); return [ '#markup' => new FormattableMarkup('User @id can administer modules.', ['@id' => $user3->id()]), '#access' => $user3->hasPermission('administer modules'), ]; } }