class AccessPolicyProcessor

Same name in other branches
  1. 10 core/lib/Drupal/Core/Session/AccessPolicyProcessor.php \Drupal\Core\Session\AccessPolicyProcessor

Processes access policies into permissions for an account.

Hierarchy

Expanded class hierarchy of AccessPolicyProcessor

1 file declares its use of AccessPolicyProcessor
AccessPolicyProcessorTest.php in core/tests/Drupal/Tests/Core/Session/AccessPolicyProcessorTest.php

File

core/lib/Drupal/Core/Session/AccessPolicyProcessor.php, line 12

Namespace

Drupal\Core\Session
View source
class AccessPolicyProcessor implements AccessPolicyProcessorInterface {
    
    /**
     * The access policies.
     *
     * @var \Drupal\Core\Session\AccessPolicyInterface[]
     */
    protected array $accessPolicies = [];
    public function __construct(VariationCacheInterface $variationCache, VariationCacheInterface $variationStatic, CacheBackendInterface $static, AccountProxyInterface $currentUser, AccountSwitcherInterface $accountSwitcher) {
    }
    
    /**
     * {@inheritdoc}
     */
    public function addAccessPolicy(AccessPolicyInterface $access_policy) : void {
        $this->accessPolicies[] = $access_policy;
    }
    
    /**
     * {@inheritdoc}
     */
    public function processAccessPolicies(AccountInterface $account, string $scope = AccessPolicyInterface::SCOPE_DRUPAL) : CalculatedPermissionsInterface {
        $persistent_cache_contexts = $this->getPersistentCacheContexts($scope);
        $initial_cacheability = (new CacheableMetadata())->addCacheContexts($persistent_cache_contexts);
        $cache_keys = [
            'access_policies',
            $scope,
        ];
        // Whether to switch the user account during cache storage and retrieval.
        //
        // This is necessary because permissions may be stored varying by the user
        // cache context or one of its child contexts. Because we may be calculating
        // permissions for an account other than the current user, we need to ensure
        // that the cache ID for said entry is set according to the passed in
        // account's data.
        //
        // Sadly, there is currently no way to reuse the cache context logic outside
        // of the caching layer. If we every get a system that allows us to process
        // cache contexts with a provided environmental value (such as the current
        // user), then we should update the logic below to use that instead.
        //
        // For the time being, we set the current user to the passed in account if
        // they differ, calculate the permissions and then immediately switch back.
        // It's the cleanest solution we could come up with that doesn't involve
        // copying half of the caching layer and that still allows us to use the
        // VariationCache for accounts other than the current user.
        $switch_account = FALSE;
        if ($this->currentUser
            ->id() !== $account->id()) {
            foreach ($persistent_cache_contexts as $cache_context) {
                [
                    $cache_context_root,
                ] = explode('.', $cache_context, 2);
                if ($cache_context_root === 'user') {
                    $switch_account = TRUE;
                    $this->accountSwitcher
                        ->switchTo($account);
                    break;
                }
            }
        }
        // Wrap the whole cache retrieval or calculation in a try-finally so that we
        // always switch back to the original account after the return statement or
        // if an exception was thrown.
        try {
            // Retrieve the permissions from the static cache if available.
            if ($static_cache = $this->variationStatic
                ->get($cache_keys, $initial_cacheability)) {
                return $static_cache->data;
            }
            // Retrieve the permissions from the persistent cache if available.
            if ($cache = $this->variationCache
                ->get($cache_keys, $initial_cacheability)) {
                $calculated_permissions = $cache->data;
                $cacheability = CacheableMetadata::createFromObject($calculated_permissions);
                // Convert the calculated permissions into an immutable value object and
                // store it in the static cache so that we don't have to do the same
                // conversion every time we call for the calculated permissions from a
                // warm static cache.
                $calculated_permissions = new CalculatedPermissions($calculated_permissions);
                $this->variationStatic
                    ->set($cache_keys, $calculated_permissions, $cacheability, $initial_cacheability);
                return $calculated_permissions;
            }
            // Otherwise build the permissions from scratch.
            // Build mode, allow all access policies to add initial data.
            $calculated_permissions = new RefinableCalculatedPermissions();
            foreach ($this->accessPolicies as $access_policy) {
                if (!$access_policy->applies($scope)) {
                    continue;
                }
                $policy_permissions = $access_policy->calculatePermissions($account, $scope);
                if (!$this->validateScope($scope, $policy_permissions)) {
                    throw new AccessPolicyScopeException(sprintf('The access policy "%s" returned permissions for scopes other than "%s".', get_class($access_policy), $scope));
                }
                $calculated_permissions = $calculated_permissions->merge($policy_permissions);
            }
            // Alter mode, allow all access policies to alter the complete build.
            foreach ($this->accessPolicies as $access_policy) {
                if (!$access_policy->applies($scope)) {
                    continue;
                }
                $access_policy->alterPermissions($account, $scope, $calculated_permissions);
                if (!$this->validateScope($scope, $calculated_permissions)) {
                    throw new AccessPolicyScopeException(sprintf('The access policy "%s" altered permissions in a scope other than "%s".', get_class($access_policy), $scope));
                }
            }
            // Apply a cache tag to easily flush the calculated permissions.
            $calculated_permissions->addCacheTags([
                'access_policies',
            ]);
            // First store the actual calculated permissions in the persistent cache,
            // along with the final cache contexts after all calculations have run. We
            // need to store the RefinableCalculatedPermissions in the persistent
            // cache, so we can still get the final cacheability from it for when we
            // run into a persistent cache hit but not a static one. At that point, if
            // we had stored a CalculatedPermissions object, we would no longer be
            // able to ask for its cache contexts.
            $cacheability = CacheableMetadata::createFromObject($calculated_permissions);
            $this->variationCache
                ->set($cache_keys, $calculated_permissions, $cacheability, $initial_cacheability);
            // Then convert the calculated permissions to an immutable value object
            // and store it in the static cache so that we don't have to do the same
            // conversion every time we call for the calculated permissions from a
            // warm static cache.
            $calculated_permissions = new CalculatedPermissions($calculated_permissions);
            $this->variationStatic
                ->set($cache_keys, $calculated_permissions, $cacheability, $initial_cacheability);
            // Return the permissions as an immutable value object.
            return $calculated_permissions;
        } finally {
            if ($switch_account) {
                $this->accountSwitcher
                    ->switchBack();
            }
        }
    }
    
    /**
     * Gets the persistent cache contexts of all policies within a given scope.
     *
     * @param string $scope
     *   The scope to get the persistent cache contexts for.
     *
     * @return string[]
     *   The persistent cache contexts of all policies within the scope.
     */
    protected function getPersistentCacheContexts(string $scope) : array {
        $cid = 'access_policies:access_policy_processor:contexts:' . $scope;
        // Retrieve the contexts from the regular static cache if available.
        if ($static_cache = $this->static
            ->get($cid)) {
            return $static_cache->data;
        }
        $contexts = [];
        foreach ($this->accessPolicies as $access_policy) {
            if ($access_policy->applies($scope)) {
                $contexts[] = $access_policy->getPersistentCacheContexts();
            }
        }
        $contexts = array_merge(...$contexts);
        // Store the contexts in the regular static cache.
        $this->static
            ->set($cid, $contexts);
        return $contexts;
    }
    
    /**
     * Validates if calculated permissions all match a single scope.
     *
     * @param string $scope
     *   The scope to match.
     * @param \Drupal\Core\Session\CalculatedPermissionsInterface $calculated_permissions
     *   The calculated permissions that should match the scope.
     *
     * @return bool
     *   Whether the calculated permissions match the scope.
     */
    protected function validateScope(string $scope, CalculatedPermissionsInterface $calculated_permissions) : bool {
        $actual_scopes = $calculated_permissions->getScopes();
        return empty($actual_scopes) || $actual_scopes === [
            $scope,
        ];
    }

}

Members

Title Sort descending Modifiers Object type Summary Overriden Title
AccessPolicyProcessor::$accessPolicies protected property The access policies.
AccessPolicyProcessor::addAccessPolicy public function Adds an access policy. Overrides AccessPolicyProcessorInterface::addAccessPolicy
AccessPolicyProcessor::getPersistentCacheContexts protected function Gets the persistent cache contexts of all policies within a given scope.
AccessPolicyProcessor::processAccessPolicies public function Processes the access policies for an account within a given scope. Overrides AccessPolicyProcessorInterface::processAccessPolicies
AccessPolicyProcessor::validateScope protected function Validates if calculated permissions all match a single scope.
AccessPolicyProcessor::__construct public function

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.