Commit 809b361f authored by Dries's avatar Dries

Issue #2222719 by effulgentsia, tim.plunkett, xjm, dawehner: Use parameter...

Issue #2222719 by effulgentsia, tim.plunkett, xjm, dawehner: Use parameter matching via reflection for access checks instead of pulling variables from request attributes.
parent 7fd3e0f9
......@@ -515,9 +515,11 @@ services:
csrf_token:
class: Drupal\Core\Access\CsrfTokenGenerator
arguments: ['@private_key']
access_arguments_resolver:
class: Drupal\Core\Access\AccessArgumentsResolver
access_manager:
class: Drupal\Core\Access\AccessManager
arguments: ['@router.route_provider', '@url_generator', '@paramconverter_manager']
arguments: ['@router.route_provider', '@url_generator', '@paramconverter_manager', '@access_arguments_resolver']
calls:
- [setContainer, ['@service_container']]
- [setRequest, ['@?request']]
......@@ -552,7 +554,7 @@ services:
- { name: access_check, applies_to: _access_theme }
access_check.custom:
class: Drupal\Core\Access\CustomAccessCheck
arguments: ['@controller_resolver']
arguments: ['@controller_resolver', '@access_arguments_resolver']
tags:
- { name: access_check, applies_to: _custom_access }
access_check.csrf:
......
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessArgumentsResolver.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Resolves the arguments to pass to an access check callable.
*/
class AccessArgumentsResolver implements AccessArgumentsResolverInterface {
/**
* {@inheritdoc}
*/
public function getArguments(callable $callable, Route $route, Request $request, AccountInterface $account) {
$arguments = array();
foreach ($this->getReflector($callable)->getParameters() as $parameter) {
$arguments[] = $this->getArgument($parameter, $route, $request, $account);
}
return $arguments;
}
/**
* Returns the argument value for a parameter.
*
* @param \ReflectionParameter $parameter
* The parameter of a callable to get the value for.
* @param \Symfony\Component\Routing\Route $route
* The access checked route.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*
* @return mixed
* The value of the requested parameter value.
*
* @throws \RuntimeException
* Thrown when there is a missing parameter.
*/
protected function getArgument(\ReflectionParameter $parameter, Route $route, Request $request, AccountInterface $account) {
$upcasted_route_arguments = $request->attributes->all();
$raw_route_arguments = isset($upcasted_route_arguments['_raw_variables']) ? $upcasted_route_arguments['_raw_variables']->all() : $upcasted_route_arguments;
$parameter_type_hint = $parameter->getClass();
$parameter_name = $parameter->getName();
// @todo Remove this once AccessManager::checkNamedRoute() is fixed to not
// leak _raw_variables from the request being duplicated.
// @see https://drupal.org/node/2265939
$raw_route_arguments += $upcasted_route_arguments;
// If the route argument exists and is NULL, return it, regardless of
// parameter type hint.
if (!isset($upcasted_route_arguments[$parameter_name]) && array_key_exists($parameter_name, $upcasted_route_arguments)) {
return NULL;
}
if ($parameter_type_hint) {
// If the argument exists and complies with the type hint, return it.
if (isset($upcasted_route_arguments[$parameter_name]) && is_object($upcasted_route_arguments[$parameter_name]) && $parameter_type_hint->isInstance($upcasted_route_arguments[$parameter_name])) {
return $upcasted_route_arguments[$parameter_name];
}
// Otherwise, resolve $request, $route, and $account by type matching
// only. This way, the callable may rename them in case the route
// defines other parameters with these names.
foreach (array($request, $route, $account) as $special_argument) {
if ($parameter_type_hint->isInstance($special_argument)) {
return $special_argument;
}
}
}
elseif (isset($raw_route_arguments[$parameter_name])) {
return $raw_route_arguments[$parameter_name];
}
// If the callable provides a default value, use it.
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
// Can't resolve it: call a method that throws an exception or can be
// overridden to do something else.
return $this->handleUnresolvedArgument($parameter);
}
/**
* Returns a reflector for the access check callable.
*
* The access checker may be either a procedural function (in which case the
* callable is the function name) or a method (in which case the callable is
* an array of the object and method name).
*
* @param callable $callable
* The callable (either a function or a method).
*
* @return \ReflectionFunctionAbstract
* The ReflectionMethod or ReflectionFunction to introspect the callable.
*/
protected function getReflector(callable $callable) {
return is_array($callable) ? new \ReflectionMethod($callable[0], $callable[1]) : new \ReflectionFunction($callable);
}
/**
* Handles unresolved arguments for getArgument().
*
* Subclasses that override this method may return a default value
* instead of throwing an exception.
*
* @throws \RuntimeException
* Thrown when there is a missing parameter.
*/
protected function handleUnresolvedArgument(\ReflectionParameter $parameter) {
$class = $parameter->getDeclaringClass();
$function = $parameter->getDeclaringFunction();
if ($class && !$function->isClosure()) {
$function_name = $class->getName() . '::' . $function->getName();
}
else {
$function_name = $function->getName();
}
throw new \RuntimeException(sprintf('Access callable "%s" requires a value for the "$%s" argument.', $function_name, $parameter->getName()));
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessArgumentsResolverInterface.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Resolves the arguments to pass to an access check callable.
*/
interface AccessArgumentsResolverInterface {
/**
* Returns the arguments to pass to the access check callable.
*
* @param callable $callable
* A PHP callable.
* @param \Symfony\Component\Routing\Route $route
* The route to check access to.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return array
* An array of arguments to pass to the callable.
*
* @throws \RuntimeException
* When a value for an argument given is not provided.
*/
public function getArguments(callable $callable, Route $route, Request $request, AccountInterface $account);
}
......@@ -7,9 +7,6 @@
namespace Drupal\Core\Access;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Provides access check results.
*/
......
......@@ -9,7 +9,6 @@
use Drupal\Core\ParamConverter\ParamConverterManagerInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Drupal\Core\Routing\RequestHelper;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountInterface;
......@@ -45,6 +44,13 @@ class AccessManager implements ContainerAwareInterface {
*/
protected $checks;
/**
* Array of access check method names keyed by service ID.
*
* @var array
*/
protected $checkMethods = array();
/**
* An array to map static requirement keys to service IDs.
*
......@@ -80,6 +86,13 @@ class AccessManager implements ContainerAwareInterface {
*/
protected $paramConverterManager;
/**
* The access arguments resolver.
*
* @var \Drupal\Core\Access\AccessArgumentsResolverInterface
*/
protected $argumentsResolver;
/**
* A request object.
*
......@@ -96,11 +109,14 @@ class AccessManager implements ContainerAwareInterface {
* The url generator.
* @param \Drupal\Core\ParamConverter\ParamConverterManagerInterface $paramconverter_manager
* The param converter manager.
* @param \Drupal\Core\Access\AccessArgumentsResolverInterface $arguments_resolver
* The access arguments resolver.
*/
public function __construct(RouteProviderInterface $route_provider, UrlGeneratorInterface $url_generator, ParamConverterManagerInterface $paramconverter_manager) {
public function __construct(RouteProviderInterface $route_provider, UrlGeneratorInterface $url_generator, ParamConverterManagerInterface $paramconverter_manager, AccessArgumentsResolverInterface $arguments_resolver) {
$this->routeProvider = $route_provider;
$this->urlGenerator = $url_generator;
$this->paramConverterManager = $paramconverter_manager;
$this->argumentsResolver = $arguments_resolver;
}
/**
......@@ -121,12 +137,15 @@ public function setRequest(Request $request) {
*
* @param string $service_id
* The ID of the service in the Container that provides a check.
* @param string $service_method
* The method to invoke on the service object for performing the check.
* @param array $applies_checks
* (optional) An array of route requirement keys the checker service applies
* to.
*/
public function addCheckService($service_id, array $applies_checks = array()) {
public function addCheckService($service_id, $service_method, array $applies_checks = array()) {
$this->checkIds[] = $service_id;
$this->checkMethods[$service_id] = $service_method;
foreach ($applies_checks as $applies_check) {
$this->staticRequirementMap[$applies_check][] = $service_id;
}
......@@ -267,11 +286,7 @@ protected function checkAll(array $checks, Route $route, Request $request, Accou
$this->loadCheck($service_id);
}
$service_access = $this->checks[$service_id]->access($route, $request, $account);
if (!in_array($service_access, array(AccessInterface::ALLOW, AccessInterface::DENY, AccessInterface::KILL), TRUE)) {
throw new AccessException("Access error in $service_id. Access services can only return AccessInterface::ALLOW, AccessInterface::DENY, or AccessInterface::KILL constants.");
}
$service_access = $this->performCheck($service_id, $route, $request, $account);
if ($service_access === AccessInterface::ALLOW) {
$access = TRUE;
......@@ -310,11 +325,7 @@ protected function checkAny(array $checks, $route, $request, AccountInterface $a
$this->loadCheck($service_id);
}
$service_access = $this->checks[$service_id]->access($route, $request, $account);
if (!in_array($service_access, array(AccessInterface::ALLOW, AccessInterface::DENY, AccessInterface::KILL), TRUE)) {
throw new AccessException("Access error in $service_id. Access services can only return AccessInterface::ALLOW, AccessInterface::DENY, or AccessInterface::KILL constants.");
}
$service_access = $this->performCheck($service_id, $route, $request, $account);
if ($service_access === AccessInterface::ALLOW) {
$access = TRUE;
......@@ -327,11 +338,46 @@ protected function checkAny(array $checks, $route, $request, AccountInterface $a
return $access;
}
/**
* Performs the specified access check.
*
* @param string $service_id
* The access check service ID to use.
* @param \Symfony\Component\Routing\Route $route
* The route to check access to.
* @param \Symfony\Component\HttpFoundation\Request $request
* The incoming request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
*
* @throws \Drupal\Core\Access\AccessException
* Thrown when the access check returns an invalid value.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
protected function performCheck($service_id, $route, $request, $account) {
$callable = array($this->checks[$service_id], $this->checkMethods[$service_id]);
$arguments = $this->argumentsResolver->getArguments($callable, $route, $request, $account);
$service_access = call_user_func_array($callable, $arguments);
if (!in_array($service_access, array(AccessInterface::ALLOW, AccessInterface::DENY, AccessInterface::KILL), TRUE)) {
throw new AccessException("Access error in $service_id. Access services can only return AccessInterface::ALLOW, AccessInterface::DENY, or AccessInterface::KILL constants.");
}
return $service_access;
}
/**
* Lazy-loads access check services.
*
* @param string $service_id
* The service id of the access check service to load.
*
* @throws \InvalidArgumentException
* Thrown when the service hasn't been registered in addCheckService().
* @throws \Drupal\Core\Access\AccessException
* Thrown when the service doesn't implement the required interface.
*/
protected function loadCheck($service_id) {
if (!in_array($service_id, $this->checkIds)) {
......@@ -340,9 +386,12 @@ protected function loadCheck($service_id) {
$check = $this->container->get($service_id);
if (!($check instanceof RoutingAccessInterface)) {
if (!($check instanceof AccessInterface)) {
throw new AccessException('All access checks must implement AccessInterface.');
}
if (!is_callable(array($check, $this->checkMethods[$service_id]))) {
throw new AccessException(sprintf('Access check method %s in service %s must be callable.', $this->checkMethods[$service_id], $service_id));
}
$this->checks[$service_id] = $check;
}
......
......@@ -7,7 +7,6 @@
namespace Drupal\Core\Access;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
......@@ -39,9 +38,17 @@ function __construct(CsrfTokenGenerator $csrf_token) {
}
/**
* {@inheritdoc}
* Checks access based on a CSRF token for the request.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
public function access(Route $route, Request $request) {
// If this is the controller request, check CSRF access as normal.
if ($request->attributes->get('_controller_request')) {
return $this->csrfToken->validate($request->query->get('token'), $request->attributes->get('_system_path')) ? static::ALLOW : static::KILL;
......
......@@ -32,26 +32,43 @@ class CustomAccessCheck implements RoutingAccessInterface {
*/
protected $controllerResolver;
/**
* The arguments resolver.
*
* @var \Drupal\Core\Access\AccessArgumentsResolverInterface
*/
protected $argumentsResolver;
/**
* Constructs a CustomAccessCheck instance.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Access\AccessArgumentsResolverInterface $arguments_resolver
* The arguments resolver.
*/
public function __construct(ControllerResolverInterface $controller_resolver) {
public function __construct(ControllerResolverInterface $controller_resolver, AccessArgumentsResolverInterface $arguments_resolver) {
$this->controllerResolver = $controller_resolver;
$this->argumentsResolver = $arguments_resolver;
}
/**
* {@inheritdoc}
* Checks access for the account and route using the custom access checker.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
$access_controller = $route->getRequirement('_custom_access');
$controller = $this->controllerResolver->getControllerFromDefinition($access_controller);
$arguments = $this->controllerResolver->getArguments($request, $controller);
return call_user_func_array($controller, $arguments);
$callable = $this->controllerResolver->getControllerFromDefinition($route->getRequirement('_custom_access'));
$arguments = $this->argumentsResolver->getArguments($callable, $route, $request, $account);
return call_user_func_array($callable, $arguments);
}
}
......@@ -7,10 +7,8 @@
namespace Drupal\Core\Access;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Allows access to routes to be controlled by an '_access' boolean parameter.
......@@ -18,9 +16,15 @@
class DefaultAccessCheck implements RoutingAccessInterface {
/**
* {@inheritdoc}
* Checks access to the route based on the _access parameter.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
public function access(Route $route) {
if ($route->getRequirement('_access') === 'TRUE') {
return static::ALLOW;
}
......
......@@ -27,12 +27,16 @@ public function process(ContainerBuilder $container) {
$access_manager = $container->getDefinition('access_manager');
foreach ($container->findTaggedServiceIds('access_check') as $id => $attributes) {
$applies = array();
$method = 'access';
foreach ($attributes as $attribute) {
if (isset($attribute['applies_to'])) {
$applies[] = $attribute['applies_to'];
}
if (isset($attribute['method'])) {
$method = $attribute['method'];
}
}
$access_manager->addMethodCall('addCheckService', array($id, $applies));
$access_manager->addMethodCall('addCheckService', array($id, $method, $applies));
}
}
}
......@@ -18,7 +18,7 @@
class EntityAccessCheck implements AccessInterface {
/**
* Implements \Drupal\Core\Access\AccessCheckInterface::access().
* Checks access to the entity operation on the given route.
*
* The value of the '_entity_access' key must be in the pattern
* 'entity_type.operation.' The entity type must match the {entity_type}
......@@ -29,6 +29,16 @@ class EntityAccessCheck implements AccessInterface {
* _entity_access: 'node.update'
* @endcode
* Available operations are 'view', 'update', 'create', and 'delete'.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
// Split the entity type and the operation.
......
......@@ -42,7 +42,17 @@ public function __construct(EntityManagerInterface $entity_manager) {
}
/**
* {@inheritdoc}
* Checks access to create the entity type and bundle for the given route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
list($entity_type, $bundle) = explode(':', $route->getRequirement($this->requirementsKey) . ':');
......
......@@ -8,28 +8,13 @@
namespace Drupal\Core\Routing\Access;
use Drupal\Core\Access\AccessInterface as GenericAccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* An access check service determines access rules for particular routes.
*/
interface AccessInterface extends GenericAccessInterface {
/**
* Checks for access to a route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return bool|null
* self::ALLOW, self::DENY, or self::KILL.
*/
public function access(Route $route, Request $request, AccountInterface $account);
// @todo Remove this interface since it no longer defines any methods?
// @see https://drupal.org/node/2266817.
}
......@@ -8,24 +8,27 @@
namespace Drupal\Core\Theme;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Access check for a theme.
* Provides access checking for themes for routing and theme negotiation.
*/
class ThemeAccessCheck implements AccessInterface {
/**
* {@inheritdoc}
* Checks access to the theme for routing.
*
* @param string $theme
* The name of a theme.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
return $this->checkAccess($request->attributes->get('theme')) ? static::ALLOW : static::DENY;
public function access($theme) {
return $this->checkAccess($theme) ? static::ALLOW : static::DENY;
}
/**
* Checks access to a theme.
* Indicates whether the theme is accessible based on whether it is enabled.
*
* @param string $theme
* The name of a theme.
......
......@@ -9,9 +9,7 @@
use Drupal\book\BookManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Drupal\node\NodeInterface;
/**
* Determines whether the requested node can be removed from its book.
......@@ -36,14 +34,16 @@ public function __construct(BookManagerInterface $book_manager) {
}
/**
* {@inheritdoc}
* Checks access for removing the node from its book.
*
* @param \Drupal\node\NodeInterface $node
* The node requested to be removed from its book.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
$node = $request->attributes->get('node');
if (!empty($node)) {
return $this->bookManager->checkNodeIsRemovable($node) ? static::ALLOW : static::DENY;
}
return static::DENY;
public function access(NodeInterface $node) {
return $this->bookManager->checkNodeIsRemovable($node) ? static::ALLOW : static::DENY;
}
}
......@@ -43,7 +43,17 @@ public function __construct(ConfigMapperManagerInterface $config_mapper_manager)
}
/**
* {@inheritdoc}
* Checks access to the overview based on permissions and translatability.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
/** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
......
......@@ -11,8 +11,7 @@
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\UserDataInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
use Drupal\user\UserInterface;
/**
* Access check for contact_personal_page route.
......@@ -47,10 +46,18 @@ public function __construct(ConfigFactoryInterface $config_factory, UserDataInte
}
/**
* {@inheritdoc}
* Checks access to the given user's contact page.
*
* @param \Drupal\user\UserInterface $user
* The user being contacted.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return string
* A \Drupal\Core\Access\AccessInterface constant value.
*/
public function access(Route $route, Request $request, AccountInterface $account) {
$contact_account = $request->attributes->get('user');
public function access(UserInterface $user, AccountInterface $account) {
$contact_account = $user;
// Anonymous users cannot have contact forms.
if ($contact_account->isAnonymous()) {
......
......@@ -37,14 +37,30 @@ public function __construct(EntityManagerInterface $manager) {
}
/**
* {@inheritdoc}
* Checks translation access for the entity and operation on the given route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.