Commit ea9f4ede authored by catch's avatar catch
Browse files

fix: #3576074 Current user is changed unexpectedly

By: claudiu.cristea
By: godotislate
(cherry picked from commit e50aeefc)
parent 5af44d4a
Loading
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
@@ -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.
@@ -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];
+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'),
    ];
  }

}