Commit 531f95eb authored by alexpott's avatar alexpott

Issue #2286971 by znerol, Berdir, almaudoh, cilefen: Remove dependency of...

Issue #2286971 by znerol, Berdir, almaudoh, cilefen: Remove dependency of current_user on request and authentication manager
parent 98366a9e
......@@ -47,10 +47,17 @@
* The killswitch in settings.php overrides all else, otherwise, the user must
* have access to the 'administer software updates' permission.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The incoming request.
*
* @return bool
* TRUE if the current user can run authorize.php, and FALSE if not.
*/
function authorize_access_allowed() {
function authorize_access_allowed(Request $request) {
$account = \Drupal::service('authentication')->authenticate($request);
if ($account) {
\Drupal::currentUser()->setAccount($account);
}
return Settings::get('allow_authorize_operations', TRUE) && \Drupal::currentUser()->hasPermission('administer software updates');
}
......@@ -79,7 +86,7 @@ function authorize_access_allowed() {
$show_messages = TRUE;
$response = new Response();
if (authorize_access_allowed()) {
if (authorize_access_allowed($request)) {
// Load both the Form API and Batch API.
require_once __DIR__ . '/includes/form.inc';
require_once __DIR__ . '/includes/batch.inc';
......
......@@ -733,11 +733,6 @@ services:
tags:
- { name: route_enhancer }
- { name: event_subscriber }
route_enhancer.authentication:
class: Drupal\Core\Routing\Enhancer\AuthenticationEnhancer
tags:
- { name: route_enhancer, priority: 1000 }
arguments: ['@authentication', '@current_user']
route_enhancer.entity:
class: Drupal\Core\Entity\Enhancer\EntityRouteEnhancer
tags:
......@@ -1110,15 +1105,14 @@ services:
- { name: service_collector, tag: authentication_provider, call: addProvider }
authentication_subscriber:
class: Drupal\Core\EventSubscriber\AuthenticationSubscriber
arguments: ['@authentication', '@current_user']
tags:
- { name: event_subscriber }
arguments: ['@authentication']
account_switcher:
class: Drupal\Core\Session\AccountSwitcher
arguments: ['@current_user', '@session_handler.write_safe']
current_user:
class: Drupal\Core\Session\AccountProxy
arguments: ['@authentication', '@request_stack']
session_configuration:
class: Drupal\Core\Session\SessionConfiguration
arguments: ['%session.storage.options%']
......
<?php
/**
* @file
* Contains Drupal\Core\Authentication\AuthenticationManagerInterface.
*/
namespace Drupal\Core\Authentication;
/**
* Defines an interface for authentication managers.
*/
interface AuthenticationManagerInterface extends AuthenticationProviderInterface {
/**
* Returns the service id of the default authentication provider.
*
* @return string
* The service id of the default authentication provider.
*/
public function defaultProviderId();
}
<?php
/**
* @file
* Contains \Drupal\Core\Authentication\AuthenticationProviderChallengeInterface
*/
namespace Drupal\Core\Authentication;
use Symfony\Component\HttpFoundation\Request;
/**
* Generate a challenge when access is denied for unauthenticated users.
*
* On a 403 (access denied), if there are no credentials on the request, some
* authentication methods (e.g. basic auth) require that a challenge is sent to
* the client.
*/
interface AuthenticationProviderChallengeInterface {
/**
* Constructs an exception which is used to generate the challenge.
*
* @var \Symfony\Component\HttpFoundation\Request
* The request.
* @var \Exception $exception
* The previous exception.
*
* @return \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface|NULL
* An exception to be used in order to generate an authentication challenge.
*/
public function challengeException(Request $request, \Exception $previous);
}
<?php
/**
* @file
* Contains \Drupal\Core\Authentication\AuthenticationProviderFilterInterface
*/
namespace Drupal\Core\Authentication;
use Symfony\Component\HttpFoundation\Request;
/**
* Restrict authentication methods to a subset of the site.
*
* Some authentication methods should not be available throughout a whole site.
* E.g., there are good reasons to restrict insecure methods like HTTP basic
* auth or an URL token authentication method to API-only routes.
*/
interface AuthenticationProviderFilterInterface {
/**
* Checks whether the authentication method is allowed on a given route.
*
* While authentication itself is run before routing, this method is called
* after routing, hence RouteMatch is available and can be used to inspect
* route options.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
* @param bool $authenticated
* Whether or not the request is authenticated.
*
* @return bool
* TRUE if an authentication method is allowed on the request, otherwise
* FALSE.
*/
public function appliesToRoutedRequest(Request $request, $authenticated);
}
......@@ -8,7 +8,6 @@
namespace Drupal\Core\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
/**
* Interface for authentication providers.
......@@ -16,52 +15,27 @@
interface AuthenticationProviderInterface {
/**
* Declares whether the provider applies to a specific request or not.
* Checks whether suitable authentication credentials are on the request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return bool
* TRUE if the provider applies to the passed request, FALSE otherwise.
* TRUE if authentication credentials suitable for this provider are on the
* request, FALSE otherwise.
*/
public function applies(Request $request);
/**
* Authenticates the user.
*
* @param \Symfony\Component\HttpFoundation\Request|null $request
* @param \Symfony\Component\HttpFoundation\Request|NULL $request
* The request object.
*
* @return \Drupal\Core\Session\AccountInterface|null
* @return \Drupal\Core\Session\AccountInterface|NULL
* AccountInterface - in case of a successful authentication.
* NULL - in case where authentication failed.
*/
public function authenticate(Request $request);
/**
* Performs cleanup tasks at the end of a request.
*
* Allow the authentication provider to clean up before the response is sent.
* This is uses for instance in \Drupal\Core\Authentication\Provider\Cookie to
* ensure the session gets committed.
*
* @param Request $request
* The request object.
*/
public function cleanup(Request $request);
/**
* Handles an exception.
*
* In case exception has happened we allow authentication providers react.
* Used in \Drupal\Core\Authentication\Provider\BasicAuth to set up headers to
* prompt login.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
*
* @return bool
* TRUE - exception handled. No need to run through other providers.
* FALSE - no actions have been done. Run through other providers.
*/
public function handleException(GetResponseForExceptionEvent $event);
}
......@@ -8,20 +8,36 @@
namespace Drupal\Core\Authentication\Provider;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\Core\Session\SessionConfigurationInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
/**
* Cookie based authentication provider.
*/
class Cookie implements AuthenticationProviderInterface {
/**
* The session configuration.
*
* @var \Drupal\Core\Session\SessionConfigurationInterface
*/
protected $sessionConfiguration;
/**
* Constructs a new cookie authentication provider.
*
* @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
* The session configuration.
*/
public function __construct(SessionConfigurationInterface $session_configuration) {
$this->sessionConfiguration = $session_configuration;
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return $request->hasSession();
return $request->hasSession() && $this->sessionConfiguration->hasSession($request);
}
/**
......@@ -29,7 +45,7 @@ public function applies(Request $request) {
*/
public function authenticate(Request $request) {
if ($request->getSession()->start()) {
// @todo Remove global in https://www.drupal.org/node/2286971
// @todo Remove global in https://www.drupal.org/node/2228393
global $_session_user;
return $_session_user;
}
......@@ -37,16 +53,4 @@ public function authenticate(Request $request) {
return NULL;
}
/**
* {@inheritdoc}
*/
public function cleanup(Request $request) {
}
/**
* {@inheritdoc}
*/
public function handleException(GetResponseForExceptionEvent $event) {
return FALSE;
}
}
......@@ -685,6 +685,11 @@ protected function initializeContainer($rebuild = FALSE) {
$this->containerNeedsDumping = FALSE;
$session_manager_started = FALSE;
if (isset($this->container)) {
// Save the id of the currently logged in user.
if ($this->container->initialized('current_user')) {
$current_user_id = $this->container->get('current_user')->id();
}
// If there is a session manager, close and save the session.
if ($this->container->initialized('session_manager')) {
$session_manager = $this->container->get('session_manager');
......@@ -731,6 +736,11 @@ protected function initializeContainer($rebuild = FALSE) {
}
}
}
if (!empty($current_user_id)) {
$this->container->get('current_user')->setInitialAccountId($current_user_id);
}
\Drupal::setContainer($this->container);
// If needs dumping flag was set, dump the container.
......
......@@ -7,71 +7,139 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Authentication\AuthenticationProviderFilterInterface;
use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Authentication subscriber.
*
* Trigger authentication and cleanup during the request.
* Trigger authentication during the request.
*/
class AuthenticationSubscriber implements EventSubscriberInterface {
/**
* Authentication provider.
*
* @var AuthenticationProviderInterface
* @var \Drupal\Core\Authentication\AuthenticationProviderInterface
*/
protected $authenticationProvider;
/**
* Keep authentication manager as private variable.
* Authentication provider filter.
*
* @var \Drupal\Core\Authentication\AuthenticationProviderFilterInterface|NULL
*/
protected $filter;
/**
* Authentication challenge provider.
*
* @param AuthenticationProviderInterface $authentication_manager
* The authentication manager.
* @var \Drupal\Core\Authentication\AuthenticationProviderChallengeInterface|NULL
*/
public function __construct(AuthenticationProviderInterface $authentication_provider) {
protected $challengeProvider;
/**
* Account proxy.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $accountProxy;
/**
* Constructs an authentication subscriber.
*
* @param \Drupal\Core\Authentication\AuthenticationProviderInterface $authentication_provider
* An authentication provider.
* @param \Drupal\Core\Session\AccountProxyInterface $account_proxy
* Account proxy.
*/
public function __construct(AuthenticationProviderInterface $authentication_provider, AccountProxyInterface $account_proxy) {
$this->authenticationProvider = $authentication_provider;
$this->filter = ($authentication_provider instanceof AuthenticationProviderFilterInterface) ? $authentication_provider : NULL;
$this->challengeProvider = ($authentication_provider instanceof AuthenticationProviderChallengeInterface) ? $authentication_provider : NULL;
$this->accountProxy = $account_proxy;
}
/**
* Triggers authentication clean up on response.
* Authenticates user on request.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The request event.
*
* @see \Drupal\Core\Authentication\AuthenticationProviderInterface::cleanup()
* @see \Drupal\Core\Authentication\AuthenticationProviderInterface::authenticate()
*/
public function onRespond(FilterResponseEvent $event) {
if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
public function onKernelRequestAuthenticate(GetResponseEvent $event) {
if ($event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
$request = $event->getRequest();
$this->authenticationProvider->cleanup($request);
if ($this->authenticationProvider->applies($request)) {
$account = $this->authenticationProvider->authenticate($request);
if ($account) {
$this->accountProxy->setAccount($account);
}
}
}
}
/**
* Pass exception handling to authentication manager.
* Denies access if authentication provider is not allowed on this route.
*
* @param GetResponseForExceptionEvent $event
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
* The request event.
*/
public function onException(GetResponseForExceptionEvent $event) {
if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
$this->authenticationProvider->handleException($event);
public function onKernelRequestFilterProvider(GetResponseEvent $event) {
if (isset($this->filter) && $event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
$request = $event->getRequest();
if ($this->authenticationProvider->applies($request) && !$this->filter->appliesToRoutedRequest($request, TRUE)) {
throw new AccessDeniedHttpException();
}
}
}
/**
* {@inheritdoc}
* Respond with a challenge on access denied exceptions if appropriate.
*
* The priority for request must be higher than the highest event subscriber
* accessing the current user.
* The priority for the response must be as low as possible allowing e.g the
* Cookie provider to send all relevant session data to the user.
* On a 403 (access denied), if there are no credentials on the request, some
* authentication methods (e.g. basic auth) require that a challenge is sent
* to the client.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
* The exception event.
*/
public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
if (isset($this->challengeProvider) && $event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
$request = $event->getRequest();
$exception = $event->getException();
if ($exception instanceof AccessDeniedHttpException && !$this->authenticationProvider->applies($request) && (!isset($this->filter) || $this->filter->appliesToRoutedRequest($request, FALSE))) {
$challenge_exception = $this->challengeProvider->challengeException($request, $exception);
if ($challenge_exception) {
$event->setException($challenge_exception);
}
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = ['onRespond', 0];
$events[KernelEvents::EXCEPTION][] = ['onException', 75];
// The priority for authentication must be higher than the highest event
// subscriber accessing the current user. Especially it must be higher than
// LanguageRequestSubscriber as LanguageManager accesses the current user if
// the language module is enabled.
$events[KernelEvents::REQUEST][] = ['onKernelRequestAuthenticate', 300];
// Access check must be performed after routing.
$events[KernelEvents::REQUEST][] = ['onKernelRequestFilterProvider', 31];
$events[KernelEvents::EXCEPTION][] = ['onExceptionSendChallenge', 75];
return $events;
}
}
......@@ -7,7 +7,6 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Component\Utility\String;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
......@@ -25,7 +24,6 @@ protected function alterRoutes(RouteCollection $collection) {
$special_variables = array(
'system_path',
'_legacy',
'_authentication_provider',
'_raw_variables',
RouteObjectInterface::ROUTE_OBJECT,
RouteObjectInterface::ROUTE_NAME,
......
......@@ -88,10 +88,6 @@ public function getContext() {
public function matchRequest(Request $request) {
$parameters = $this->chainRouter->matchRequest($request);
$request->attributes->add($parameters);
// Trigger a session start and authentication by accessing any property of
// the current user.
// @todo This will be removed in https://www.drupal.org/node/2229145.
$this->account->id();
$this->checkAccess($request);
// We can not return $parameters because the access check can change the
// request attributes.
......
<?php
/**
* @file
* Contains \Drupal\Core\Routing\Enhancer\AuthenticationEnhancer.
*/
namespace Drupal\Core\Routing\Enhancer;
use Drupal\Core\Authentication\AuthenticationManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
/**
* Authentication cleanup for incoming routes.
*
* The authentication system happens before routing, so all authentication
* providers will attempt to authorize a user. However, not all routes allow
* all authentication mechanisms. Instead, we check if the used provider is
* valid for the matched route and if not, force the user to anonymous.
*/
class AuthenticationEnhancer implements RouteEnhancerInterface {
/**
* The authentication manager.
*
* @var \Drupal\Core\Authentication\AuthenticationManager
*/
protected $manager;
/**
* The current user service.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* Constructs a AuthenticationEnhancer object.
*
* @param \Drupal\Core\Authentication\AuthenticationManagerInterface $manager
* The authentication manager.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The current user service.
*/
function __construct(AuthenticationManagerInterface $manager, AccountProxyInterface $current_user) {
$this->manager = $manager;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public function enhance(array $defaults, Request $request) {
$auth_provider_triggered = $request->attributes->get('_authentication_provider');
if (!empty($auth_provider_triggered)) {
$route = isset($defaults[RouteObjectInterface::ROUTE_OBJECT]) ? $defaults[RouteObjectInterface::ROUTE_OBJECT] : NULL;
$auth_providers = ($route && $route->getOption('_auth')) ? $route->getOption('_auth') : array($this->manager->defaultProviderId());
// If the request was authenticated with a non-permitted provider,
// force the user back to anonymous.
if (!in_array($auth_provider_triggered, $auth_providers)) {
$anonymous_user = new AnonymousUserSession();
$this->currentUser->setAccount($anonymous_user);
}
}
return $defaults;
}
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
return TRUE;
}
}
......@@ -7,9 +7,6 @@
namespace Drupal\Core\Session;
use Drupal\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* A proxied implementation of AccountInterface.
*
......@@ -23,20 +20,6 @@
*/
class AccountProxy implements AccountProxyInterface {
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The authentication manager.
*
* @var \Drupal\Core\Authentication\AuthenticationManagerInterface
*/
protected $authenticationManager;
/**
* The instantiated account.
*
......@@ -45,17 +28,11 @@ class AccountProxy implements AccountProxyInterface {
protected $account;
/**
* Constructs a new AccountProxy.
* Initial account id.
*
* @param \Drupal\Core\Authentication\AuthenticationManagerInterface $authentication_manager
* The authentication manager.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object used for authenticating.
* @var int
*/
public function __construct(AuthenticationManagerInterface $authentication_manager, RequestStack $requestStack) {
$this->authenticationManager = $authentication_manager;
$this->requestStack = $requestStack;
}
protected $initialAccountId;
/**
* {@inheritdoc}
......@@ -75,10 +52,17 @@ public function setAccount(AccountInterface $account) {
*/
public function getAccount() {
if (!isset($this->account)) {
// Use the master request to prevent subrequests authenticating to a
// different user.
$this->setAccount($this->authenticationManager->authenticate($this->requestStack->getMasterRequest()));
if ($this->initialAccountId) {
// After the container is rebuilt, DrupalKernel sets the initial
// account to the id of the logged in user. This is necessary in order
// to refresh the user account reference here.
$this->account = $this->loadUserEntity($this->initialAccountId);
}
else {
$this->account = new AnonymousUserSession();
}
}
return $this->account;
}
......@@ -187,5 +171,38 @@ public function getLastAccessedTime() {
return $this->getAccount()->getLastAccessedTime();
}
}
/**
* {@inheritdoc}
*/
public function setInitialAccountId($account_id) {
if (isset($this->account)) {
throw new \LogicException('AccountProxyInterface::setInitialAccountId() cannot be called after an account was set on the AccountProxy');
}
$this->initialAccountId = $account_id;
}
/**
* Load a user entity.
*
* The entity manager requires additional initialization code and cache
* clearing after the list of modules is changed. Therefore it is necessary to
* retrieve it as late as possible.
*
* Because of serialization issues it is currently not possible to inject the
* container into the AccountProxy. Thus it is necessary to retrieve the
* entity manager statically.
*
* @see https://www.drupal.org/node/2430447
*
* @param int $account_id
* The id of an account to load.
*
* @return \Drupal\Core\Session\AccountInterface|NULL
* An account or NULL if none is found.
*/
protected function loadUserEntity($account_id) {
return \Drupal::entityManager()->getStorage('user')->load($account_id);
}
}
......@@ -37,5 +37,15 @@ public function setAccount(AccountInterface $account);
*/
public function getAccount();
}
/**
* Sets the id of the initial account.
*
* Never use this method, its sole purpose is to work around weird effects
* during mid-request container rebuilds.
*
* @param int $account_id