AccessPolicyProcessor.php

Same filename and directory in other branches
  1. 11.x core/lib/Drupal/Core/Session/AccessPolicyProcessor.php

Namespace

Drupal\Core\Session

File

core/lib/Drupal/Core/Session/AccessPolicyProcessor.php

View source
<?php

namespace Drupal\Core\Session;

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\VariationCacheInterface;

/**
 * Processes access policies into permissions for an account.
 */
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,
        ];
    }

}

Classes

Title Deprecated Summary
AccessPolicyProcessor Processes access policies into permissions for an account.

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