diff --git a/core/authorize.php b/core/authorize.php index 0d61004..35277f6 100644 --- a/core/authorize.php +++ b/core/authorize.php @@ -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'; diff --git a/core/core.services.yml b/core/core.services.yml index 6681e7d..d49416d 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -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%'] @@ -1135,7 +1129,7 @@ services: alias: session_handler.storage session_handler.storage: class: Drupal\Core\Session\SessionHandler - arguments: ['@request_stack', '@database'] + arguments: ['@request_stack', '@database', '@current_user'] tags: - { name: backend_overridable } session_handler.write_check: diff --git a/core/lib/Drupal/Core/Authentication/AuthenticationManager.php b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php index 3bc40d8..2886030 100644 --- a/core/lib/Drupal/Core/Authentication/AuthenticationManager.php +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php @@ -8,28 +8,24 @@ namespace Drupal\Core\Authentication; use Drupal\Core\Routing\RouteMatch; -use Drupal\Core\Session\AnonymousUserSession; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; /** * Manager for authentication. * * On each request, let all authentication providers try to authenticate the * user. The providers are iterated according to their priority and the first - * provider detecting credentials for its method will become the triggered - * provider. No further provider will get triggered. + * provider detecting credentials for its method wins. No further provider will + * get triggered. * - * If no provider was triggered the lowest-priority provider is assumed to - * be responsible. If no provider set an active user then the user is set to - * anonymous. + * If no provider set an active user then the user is set to anonymous. */ -class AuthenticationManager implements AuthenticationProviderInterface, AuthenticationManagerInterface { +class AuthenticationManager implements AuthenticationProviderInterface, AuthenticationProviderFilterInterface, AuthenticationProviderChallengeInterface { /** * Array of all registered authentication providers, keyed by ID. * - * @var array + * @var \Drupal\Core\Authentication\AuthenticationProviderInterface[] */ protected $providers; @@ -43,16 +39,45 @@ class AuthenticationManager implements AuthenticationProviderInterface, Authenti /** * Sorted list of registered providers. * - * @var array + * @var \Drupal\Core\Authentication\AuthenticationProviderInterface[] */ protected $sortedProviders; /** - * Id of the provider that authenticated the user. + * List of providers which implement the filter interface. + * + * @var \Drupal\Core\Authentication\AuthenticationProviderFilterInterface[] + */ + protected $filters; + + /** + * List of providers which implement the challenge interface. + * + * @var \Drupal\Core\Authentication\AuthenticationProviderChallengeInterface[] + */ + protected $challengers; + + /** + * List of providers which are allowed on routes with no _auth option. * - * @var string + * @var string[] */ - protected $triggeredProviderId = ''; + protected $globalProviders; + + /** + * Constructs an authentication manager. + * + * @todo Revisit service construction. Especially write a custom compiler pass + * which is capable of collecting, sorting and injecting all providers + * (including global/vs non global), filters and challengers on compile + * time in https://www.drupal.org/node/2432585. + * + * @param array $global_providers + * List of global providers, keyed by the provier ID. + */ + public function __construct($global_providers = ['cookie' => TRUE]) { + $this->globalProviders = $global_providers; + } /** * Adds a provider to the array of registered providers. @@ -72,139 +97,176 @@ public function addProvider(AuthenticationProviderInterface $provider, $id, $pri $this->providerOrders[$priority][$id] = $provider; // Force the builders to be re-sorted. $this->sortedProviders = NULL; + + if ($provider instanceof AuthenticationProviderFilterInterface) { + $this->filters[$id] = $provider; + } + if ($provider instanceof AuthenticationProviderChallengeInterface) { + $this->challengers[$id] = $provider; + } } /** * {@inheritdoc} */ public function applies(Request $request) { - return TRUE; + return (bool) $this->getProvider($request); } /** * {@inheritdoc} */ public function authenticate(Request $request) { - $account = NULL; + $provider_id = $this->getProvider($request); + return $this->providers[$provider_id]->authenticate($request); + } - // Iterate the allowed providers. - foreach ($this->filterProviders($this->getSortedProviders(), $request) as $provider_id => $provider) { - if ($provider->applies($request)) { - // Try to authenticate with this provider, skipping all others. - $account = $provider->authenticate($request); - $this->triggeredProviderId = $provider_id; - break; - } - } + /** + * {@inheritdoc} + */ + public function appliesToRoutedRequest(Request $request, $authenticated) { + $result = FALSE; - // No provider returned a valid account, so set the user to anonymous. - if (!$account) { - $account = new AnonymousUserSession(); + if ($authenticated) { + $result = $this->applyFilter($request, $authenticated, $this->getProvider($request)); } - - // No provider was fired, so assume the one with the least priority - // should have. - if (!$this->triggeredProviderId) { - $this->triggeredProviderId = $this->defaultProviderId(); + else { + foreach ($this->getSortedProviders() as $provider_id => $provider) { + if ($this->applyFilter($request, $authenticated, $provider_id)) { + $result = TRUE; + break; + } + } } - // Save the authenticated account and the provider that supplied it - // for later access. - $request->attributes->set('_authentication_provider', $this->triggeredProviderId); + return $result; + } - return $account; + /** + * {@inheritdoc} + */ + public function challengeException(Request $request, \Exception $previous) { + $provider_id = $this->getChallenger($request); + if ($provider_id) { + return $this->challengers[$provider_id]->challengeException($request, $previous); + } } /** - * Returns the default provider ID. + * Returns the id of the authentication provider for a request. * - * The default provider is the one with the lowest registered priority. + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request. * - * @return string - * The ID of the default provider. + * @return string|NULL + * The id of the first authentication provider which applies to the request. + * If no application detects appropriate credentials, then NULL is returned. */ - public function defaultProviderId() { - $providers = $this->getSortedProviders(); - $provider_ids = array_keys($providers); - return end($provider_ids); + protected function getProvider(Request $request) { + foreach ($this->getSortedProviders() as $provider_id => $provider) { + if ($provider->applies($request)) { + return $provider_id; + } + } } /** - * Returns the sorted array of authentication providers. + * Returns the id of the challenge provider for a request. * - * @return array - * An array of authentication provider objects. + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request. + * + * @return string|NULL + * The id of the first authentication provider which applies to the request. + * If no application detects appropriate credentials, then NULL is returned. */ - public function getSortedProviders() { - if (!isset($this->sortedProviders)) { - // Sort the builders according to priority. - krsort($this->providerOrders); - // Merge nested providers from $this->providers into $this->sortedProviders. - $this->sortedProviders = array(); - foreach ($this->providerOrders as $providers) { - $this->sortedProviders = array_merge($this->sortedProviders, $providers); + protected function getChallenger(Request $request) { + if (!empty($this->challengers)) { + foreach ($this->getSortedProviders($request, FALSE) as $provider_id => $provider) { + if (isset($this->challengers[$provider_id]) && !$provider->applies($request) && $this->applyFilter($request, FALSE, $provider_id)) { + return $provider_id; + } } } - return $this->sortedProviders; } /** - * Filters a list of providers and only return those allowed on the request. + * Checks whether a provider is allowed on the given request. * - * @param \Drupal\Core\Authentication\AuthenticationProviderInterface[] $providers - * An array of authentication provider objects. - * @param Request $request - * The request object. + * If no filter is registered for the given provider id, the default filter + * is applied. * - * @return \Drupal\Core\Authentication\AuthenticationProviderInterface[] - * The filtered array authentication provider objects. + * @param \Symfony\Component\HttpFoundation\Request $request + * The incoming request. + * @param bool $authenticated + * Whether or not the request is authenticated. + * @param string $provider_id + * The id of the authentication provider to check access for. + * + * @return bool + * TRUE if provider is allowed, FALSE otherwise. */ - protected function filterProviders(array $providers, Request $request) { - $route = RouteMatch::createFromRequest($request)->getRouteObject(); - $allowed_providers = array(); - if ($route && $route->hasOption('_auth')) { - $allowed_providers = $route->getOption('_auth'); + protected function applyFilter(Request $request, $authenticated, $provider_id) { + if (isset($this->filters[$provider_id])) { + $result = $this->filters[$provider_id]->appliesToRoutedRequest($request, $authenticated); } - elseif ($default_provider = $this->defaultProviderId()) { - // @todo Mirrors the defective behavior of AuthenticationEnhancer and - // restricts the list of allowed providers to the default provider if no - // _auth was specified on the current route. - // - // This restriction will be removed by https://www.drupal.org/node/2286971 - // See also https://www.drupal.org/node/2283637 - $allowed_providers = array($default_provider); + else { + $result = $this->defaultFilter($request, $provider_id); } - return array_intersect_key($providers, array_flip($allowed_providers)); + return $result; } /** - * Cleans up the authentication. + * Default implementation of the provider filter. * - * Allow the triggered provider to clean up before the response is sent, e.g. - * trigger a session commit. + * Checks whether a provider is allowed as per the _auth option on a route. If + * the option is not set or if the request did not match any route, only + * providers from the global provider set are allowed. + * + * If no filter is registered for the given provider id, the default filter + * is applied. * * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. + * The incoming request. + * @param string $provider_id + * The id of the authentication provider to check access for. * - * @see \Drupal\Core\Authentication\Provider\Cookie::cleanup() + * @return bool + * TRUE if provider is allowed, FALSE otherwise. */ - public function cleanup(Request $request) { - if (empty($this->providers[$this->triggeredProviderId])) { - return; + protected function defaultFilter(Request $request, $provider_id) { + $route = RouteMatch::createFromRequest($request)->getRouteObject(); + $has_auth_option = isset($route) && $route->hasOption('_auth'); + + if ($has_auth_option) { + return in_array($provider_id, $route->getOption('_auth')); + } + else { + return isset($this->globalProviders[$provider_id]); } - $this->providers[$this->triggeredProviderId]->cleanup($request); } /** - * {@inheritdoc} + * Returns the sorted array of authentication providers. + * + * @todo Replace with a list of providers sorted during compile time in + * https://www.drupal.org/node/2432585. + * + * @return \Drupal\Core\Authentication\AuthenticationProviderInterface[] + * An array of authentication provider objects. */ - public function handleException(GetResponseForExceptionEvent $event) { - foreach ($this->filterProviders($this->getSortedProviders(), $event->getRequest()) as $provider) { - if ($provider->handleException($event) === TRUE) { - break; + protected function getSortedProviders() { + if (!isset($this->sortedProviders)) { + // Sort the builders according to priority. + krsort($this->providerOrders); + // Merge nested providers from $this->providers into $this->sortedProviders. + $this->sortedProviders = array(); + foreach ($this->providerOrders as $providers) { + $this->sortedProviders = array_merge($this->sortedProviders, $providers); } } + return $this->sortedProviders; } } diff --git a/core/lib/Drupal/Core/Authentication/AuthenticationManagerInterface.php b/core/lib/Drupal/Core/Authentication/AuthenticationManagerInterface.php deleted file mode 100644 index 0b4c0ff..0000000 --- a/core/lib/Drupal/Core/Authentication/AuthenticationManagerInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -hasSession(); - } + protected $sessionConfiguration; /** - * {@inheritdoc} + * The database connection. + * + * @var \Drupal\Core\Database\Connection */ - public function authenticate(Request $request) { - if ($request->getSession()->start()) { - // @todo Remove global in https://www.drupal.org/node/2286971 - global $_session_user; - return $_session_user; - } + protected $connection; - return NULL; + /** + * Constructs a new cookie authentication provider. + * + * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration + * The session configuration. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + */ + public function __construct(SessionConfigurationInterface $session_configuration, Connection $connection) { + $this->sessionConfiguration = $session_configuration; + $this->connection = $connection; } /** * {@inheritdoc} */ - public function cleanup(Request $request) { + public function applies(Request $request) { + return $request->hasSession() && $this->sessionConfiguration->hasSession($request); } /** * {@inheritdoc} */ - public function handleException(GetResponseForExceptionEvent $event) { - return FALSE; + public function authenticate(Request $request) { + if ($session = $request->getSession()) { + // Handle the case of first time visitors and clients that don't store + // cookies (eg. web crawlers). + $cookies = $request->cookies; + if (!$cookies->has($session->getName())) { + return new AnonymousUserSession(); + } + + $values = $this->connection->query("SELECT u.*, s.* FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.default_langcode = 1 AND s.sid = :sid", array( + ':sid' => Crypt::hashBase64($session->getId()), + ))->fetchAssoc(); + + // We found the client's session record and they are an authenticated, + // active user. + if ($values && $values['uid'] > 0 && $values['status'] == 1) { + // Add roles element to $user. + $rids = $this->connection->query("SELECT ur.roles_target_id as rid FROM {user__roles} ur WHERE ur.entity_id = :uid", array( + ':uid' => $values['uid'], + ))->fetchCol(); + $values['roles'] = array_merge(array(DRUPAL_AUTHENTICATED_RID), $rids); + $user = new UserSession($values); + } + elseif ($values) { + // The user is anonymous or blocked. Only preserve two fields from the + // {sessions} table. + $user = new UserSession(array( + 'session' => $values['session'], + 'access' => $values['access'], + )); + } + else { + // The session has expired. + $user = new AnonymousUserSession(); + } + // Likewise, do not update access time more than once per 180 seconds. + if ($user->isAuthenticated() && REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) { + /** @var \Drupal\user\UserStorageInterface $storage */ + $storage = \Drupal::entityManager()->getStorage('user'); + $storage->updateLastAccessTimestamp($user, REQUEST_TIME); + } + return $user; + } + return NULL; } + } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index eb841b2..be9821d 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -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. diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php index 80f7796..6fd0b99 100644 --- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php @@ -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; } + } diff --git a/core/lib/Drupal/Core/EventSubscriber/SpecialAttributesRouteSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/SpecialAttributesRouteSubscriber.php index e2e7137..d096531 100644 --- a/core/lib/Drupal/Core/EventSubscriber/SpecialAttributesRouteSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/SpecialAttributesRouteSubscriber.php @@ -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, diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php index 93a31c3..1cf937c 100644 --- a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php +++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php @@ -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. diff --git a/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php deleted file mode 100644 index b1639ec..0000000 --- a/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php +++ /dev/null @@ -1,81 +0,0 @@ -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; - } - -} diff --git a/core/lib/Drupal/Core/Session/AccountProxy.php b/core/lib/Drupal/Core/Session/AccountProxy.php index 2aa8fa5..eae71e7 100644 --- a/core/lib/Drupal/Core/Session/AccountProxy.php +++ b/core/lib/Drupal/Core/Session/AccountProxy.php @@ -7,9 +7,6 @@ namespace Drupal\Core\Session; -use Drupal\Core\Authentication\AuthenticationManagerInterface; -use Symfony\Component\HttpFoundation\RequestStack; - /** * A proxied implementation of AccountInterface. * @@ -24,20 +21,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. * * @var \Drupal\Core\Session\AccountInterface @@ -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); + } +} diff --git a/core/lib/Drupal/Core/Session/AccountProxyInterface.php b/core/lib/Drupal/Core/Session/AccountProxyInterface.php index 017bd8b..7ac9758 100644 --- a/core/lib/Drupal/Core/Session/AccountProxyInterface.php +++ b/core/lib/Drupal/Core/Session/AccountProxyInterface.php @@ -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 + * The id of the initial account. + */ + public function setInitialAccountId($account_id); +} diff --git a/core/lib/Drupal/Core/Session/SessionHandler.php b/core/lib/Drupal/Core/Session/SessionHandler.php index 81877f9..05b5887 100644 --- a/core/lib/Drupal/Core/Session/SessionHandler.php +++ b/core/lib/Drupal/Core/Session/SessionHandler.php @@ -34,11 +34,11 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface { protected $connection; /** - * An associative array of obsolete sessions with session id as key, and db-key as value. + * The current user account. * - * @var array + * @var \Drupal\Core\Session\AccountInterface */ - protected $obsoleteSessionIds = array(); + protected $currentUser; /** * Constructs a new SessionHandler instance. @@ -47,10 +47,13 @@ class SessionHandler extends AbstractProxy implements \SessionHandlerInterface { * The request stack. * @param \Drupal\Core\Database\Connection $connection * The database connection. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user account. */ - public function __construct(RequestStack $request_stack, Connection $connection) { + public function __construct(RequestStack $request_stack, Connection $connection, AccountInterface $current_user) { $this->requestStack = $request_stack; $this->connection = $connection; + $this->currentUser = $current_user; } /** @@ -64,58 +67,30 @@ public function open($save_path, $name) { * {@inheritdoc} */ public function read($sid) { - // @todo Remove global in https://www.drupal.org/node/2286971 - global $_session_user; - - // Handle the case of first time visitors and clients that don't store - // cookies (eg. web crawlers). - $cookies = $this->requestStack->getCurrentRequest()->cookies; - if (empty($sid) || !$cookies->has($this->getName())) { - $_session_user = new UserSession(); - return ''; - } + // Read the session data from the database. + $record = $this->connection->select('sessions', 's') + ->fields('s') + ->condition('sid', Crypt::hashBase64($sid)) + ->execute() + ->fetchAssoc(); - $values = $this->connection->query("SELECT u.*, s.* FROM {users_field_data} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE u.default_langcode = 1 AND s.sid = :sid", array( - ':sid' => Crypt::hashBase64($sid), - ))->fetchAssoc(); - - // We found the client's session record and they are an authenticated, - // active user. - if ($values && $values['uid'] > 0 && $values['status'] == 1) { - // Add roles element to $user. - $rids = $this->connection->query("SELECT ur.roles_target_id as rid FROM {user__roles} ur WHERE ur.entity_id = :uid", array( - ':uid' => $values['uid'], - ))->fetchCol(); - $values['roles'] = array_merge(array(AccountInterface::AUTHENTICATED_ROLE), $rids); - $_session_user = new UserSession($values); - } - elseif ($values) { - // The user is anonymous or blocked. Only preserve two fields from the - // {sessions} table. - $_session_user = new UserSession(array( - 'session' => $values['session'], - 'access' => $values['access'], - )); + if (isset($record['session'])) { + return $record['session']; } else { - // The session has expired. - $_session_user = new UserSession(); + return ''; } - - return $_session_user->session; } /** * {@inheritdoc} */ public function write($sid, $value) { - $user = \Drupal::currentUser(); - // The exception handler is not active at this point, so we need to do it // manually. try { $fields = array( - 'uid' => $user->id(), + 'uid' => $this->currentUser->id(), 'hostname' => $this->requestStack->getCurrentRequest()->getClientIP(), 'session' => $value, 'timestamp' => REQUEST_TIME, @@ -124,13 +99,6 @@ public function write($sid, $value) { ->keys(array('sid' => Crypt::hashBase64($sid))) ->fields($fields) ->execute(); - - // Likewise, do not update access time more than once per 180 seconds. - if ($user->isAuthenticated() && REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) { - /** @var \Drupal\user\UserStorageInterface $storage */ - $storage = \Drupal::entityManager()->getStorage('user'); - $storage->updateLastAccessTimestamp($user, REQUEST_TIME); - } return TRUE; } catch (\Exception $exception) { @@ -156,8 +124,6 @@ public function close() { * {@inheritdoc} */ public function destroy($sid) { - - // Delete session data. $this->connection->delete('sessions') ->condition('sid', Crypt::hashBase64($sid)) @@ -166,8 +132,12 @@ public function destroy($sid) { // Reset $_SESSION and current user to prevent a new session from being // started in \Drupal\Core\Session\SessionManager::save(). $_SESSION = array(); - \Drupal::currentUser()->setAccount(new AnonymousUserSession()); + // @todo: Manipulating the current user is not the business of the session + // handler. This should be moved to the authentication mananger. + $this->currentUser->setAccount(new AnonymousUserSession()); + + // @todo: cookie management should be moved to Cookie auth manager. // Unset the session cookies. $this->deleteCookie($this->getName()); @@ -194,6 +164,8 @@ public function gc($lifetime) { * * @param string $name * Name of session cookie to delete. + * + * @todo: cookie management should be moved to Cookie auth manager. */ protected function deleteCookie($name) { $cookies = $this->requestStack->getCurrentRequest()->cookies; diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php index 706e7ab..2c6f09a 100644 --- a/core/lib/Drupal/Core/Session/SessionManager.php +++ b/core/lib/Drupal/Core/Session/SessionManager.php @@ -121,10 +121,6 @@ public function start() { } if (empty($result)) { - // @todo Remove global in https://www.drupal.org/node/2286971 - global $_session_user; - $_session_user = new AnonymousUserSession(); - // Randomly generate a session identifier for this request. This is // necessary because \Drupal\user\SharedTempStoreFactory::get() wants to // know the future session ID of a lazily started session in advance. @@ -187,7 +183,7 @@ public function save() { return; } - if ($user->isAnonymous() && $this->isSessionObsolete()) { + if (!$user || ($user->isAnonymous() && $this->isSessionObsolete())) { // There is no session data to store, destroy the session if it was // previously started. if ($this->getSaveHandler()->isActive()) { @@ -211,8 +207,6 @@ public function save() { * {@inheritdoc} */ public function regenerate($destroy = FALSE, $lifetime = NULL) { - $user = \Drupal::currentUser(); - // Nothing to do if we are not allowed to change the session. if ($this->isCli()) { return; diff --git a/core/lib/Drupal/Core/StackMiddleware/Session.php b/core/lib/Drupal/Core/StackMiddleware/Session.php index f92e778..e0ed0e4 100644 --- a/core/lib/Drupal/Core/StackMiddleware/Session.php +++ b/core/lib/Drupal/Core/StackMiddleware/Session.php @@ -54,7 +54,9 @@ public function __construct(HttpKernelInterface $http_kernel, $service_name = 's */ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) { if ($type === self::MASTER_REQUEST && PHP_SAPI !== 'cli') { - $request->setSession($this->container->get($this->sessionServiceName)); + $session = $this->container->get($this->sessionServiceName); + $session->start(); + $request->setSession($session); } $result = $this->httpKernel->handle($request, $type, $catch); diff --git a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php index 2fed1ed..c2b8483 100644 --- a/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php +++ b/core/modules/basic_auth/src/Authentication/Provider/BasicAuth.php @@ -7,21 +7,20 @@ namespace Drupal\basic_auth\Authentication\Provider; -use \Drupal\Component\Utility\String; +use Drupal\Component\Utility\String; use Drupal\Core\Authentication\AuthenticationProviderInterface; +use Drupal\Core\Authentication\AuthenticationProviderChallengeInterface; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Flood\FloodInterface; use Drupal\user\UserAuthInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * HTTP Basic authentication provider. */ -class BasicAuth implements AuthenticationProviderInterface { +class BasicAuth implements AuthenticationProviderInterface, AuthenticationProviderChallengeInterface { /** * The config factory. @@ -131,25 +130,12 @@ public function authenticate(Request $request) { /** * {@inheritdoc} */ - public function cleanup(Request $request) {} - - /** - * {@inheritdoc} - */ - public function handleException(GetResponseForExceptionEvent $event) { - $exception = $event->getException(); - if (\Drupal::currentUser()->isAnonymous() && $exception instanceof AccessDeniedHttpException) { - if (!$this->applies($event->getRequest())) { - $site_name = $this->configFactory->get('system.site')->get('name'); - global $base_url; - $challenge = String::format('Basic realm="@realm"', array( - '@realm' => !empty($site_name) ? $site_name : $base_url, - )); - $event->setException(new UnauthorizedHttpException($challenge, 'No authentication credentials provided.', $exception)); - } - return TRUE; - } - return FALSE; + public function challengeException(Request $request, \Exception $previous) { + $site_name = $this->configFactory->get('system.site')->get('name'); + $challenge = String::format('Basic realm="@realm"', array( + '@realm' => !empty($site_name) ? $site_name : 'Access restricted', + )); + return new UnauthorizedHttpException($challenge, 'No authentication credentials provided.', $previous); } } diff --git a/core/modules/block/src/Tests/BlockLanguageTest.php b/core/modules/block/src/Tests/BlockLanguageTest.php index d8e9f18..c3f87a1 100644 --- a/core/modules/block/src/Tests/BlockLanguageTest.php +++ b/core/modules/block/src/Tests/BlockLanguageTest.php @@ -149,6 +149,10 @@ public function testMultipleLanguageTypes() { $this->drupalGet('node', ['query' => ['language' => 'fr']]); $this->assertText('Powered by Drupal', 'The body of the block appears on the page.'); + // Re-login in order to clear the interface language stored in the session. + $this->drupalLogout(); + $this->drupalLogin($this->adminUser); + // Content language does not depend on session/request arguments. // It will fall back on English (site default) and not display the block. $this->drupalGet('en'); diff --git a/core/modules/locale/tests/modules/early_translation_test/src/Auth.php b/core/modules/locale/tests/modules/early_translation_test/src/Auth.php index afb5ea1..12da491 100644 --- a/core/modules/locale/tests/modules/early_translation_test/src/Auth.php +++ b/core/modules/locale/tests/modules/early_translation_test/src/Auth.php @@ -10,7 +10,6 @@ use Drupal\Core\Authentication\AuthenticationProviderInterface; use Drupal\Core\Entity\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; /** * Test authentication provider. @@ -53,16 +52,4 @@ public function authenticate(Request $request) { return NULL; } - /** - * {@inheritdoc} - */ - public function cleanup(Request $request) {} - - /** - * {@inheritdoc} - */ - public function handleException(GetResponseForExceptionEvent $event) { - return FALSE; - } - } diff --git a/core/modules/rest/rest.services.yml b/core/modules/rest/rest.services.yml index 14e4ab2..bfba7bb 100644 --- a/core/modules/rest/rest.services.yml +++ b/core/modules/rest/rest.services.yml @@ -11,6 +11,7 @@ services: arguments: [rest] access_check.rest.csrf: class: Drupal\rest\Access\CSRFAccessCheck + arguments: ['@session_configuration'] tags: - { name: access_check } rest.link_manager: diff --git a/core/modules/rest/src/Access/CSRFAccessCheck.php b/core/modules/rest/src/Access/CSRFAccessCheck.php index 563a0a4..2e39c61 100644 --- a/core/modules/rest/src/Access/CSRFAccessCheck.php +++ b/core/modules/rest/src/Access/CSRFAccessCheck.php @@ -10,6 +10,7 @@ use Drupal\Core\Access\AccessCheckInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Session\SessionConfigurationInterface; use Symfony\Component\Routing\Route; use Symfony\Component\HttpFoundation\Request; @@ -19,6 +20,23 @@ class CSRFAccessCheck implements AccessCheckInterface { /** + * The session configuration. + * + * @var \Drupal\Core\Session\SessionConfigurationInterface + */ + protected $sessionConfiguration; + + /** + * Constructs a new rest CSRF access check. + * + * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration + * The session configuration. + */ + public function __construct(SessionConfigurationInterface $session_configuration) { + $this->sessionConfiguration = $session_configuration; + } + + /** * Implements AccessCheckInterface::applies(). */ public function applies(Route $route) { @@ -54,7 +72,6 @@ public function applies(Route $route) { */ public function access(Request $request, AccountInterface $account) { $method = $request->getMethod(); - $cookie = $request->attributes->get('_authentication_provider') == 'cookie'; // This check only applies if // 1. this is a write operation @@ -62,7 +79,7 @@ public function access(Request $request, AccountInterface $account) { // 3. the request comes with a session cookie. if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE')) && $account->isAuthenticated() - && $cookie + && $this->sessionConfiguration->hasSession($request) ) { $csrf_token = $request->headers->get('X-CSRF-Token'); if (!\Drupal::csrfToken()->validate($csrf_token, 'rest')) { @@ -72,4 +89,5 @@ public function access(Request $request, AccountInterface $account) { // Let other access checkers decide if the request is legit. return AccessResult::allowed()->setCacheable(FALSE); } + } diff --git a/core/modules/rest/src/Tests/AuthTest.php b/core/modules/rest/src/Tests/AuthTest.php index c8a8e3b..09537c8 100644 --- a/core/modules/rest/src/Tests/AuthTest.php +++ b/core/modules/rest/src/Tests/AuthTest.php @@ -56,15 +56,15 @@ public function testRead() { // Try to read the resource with session cookie authentication, which is // not enabled and should not work. $this->httpRequest($entity->urlInfo(), 'GET', NULL, $this->defaultMimeType); - $this->assertResponse('401', 'HTTP response code is 401 when the request is authenticated but not authorized.'); + $this->assertResponse('403', 'HTTP response code is 403 when the request was authenticated by the wrong authentication provider.'); // Ensure that cURL settings/headers aren't carried over to next request. unset($this->curlHandle); // Now read it with the Basic authentication which is enabled and should // work. - $this->basicAuthGet($entity->urlInfo(), $account->getUsername(), $account->pass_raw); - $this->assertResponse('200', 'HTTP response code is 200 for successfully authorized requests.'); + $this->basicAuthGet($entity->urlInfo(), $account->getUsername(), $account->pass_raw, $this->defaultMimeType); + $this->assertResponse('200', 'HTTP response code is 200 for successfully authenticated requests.'); $this->curlClose(); } @@ -80,11 +80,16 @@ public function testRead() { * The user name to authenticate with. * @param string $password * The password. + * @param string $mime_type + * The MIME type for the Accept header. * * @return string * Curl output. */ - protected function basicAuthGet(Url $url, $username, $password) { + protected function basicAuthGet(Url $url, $username, $password, $mime_type = NULL) { + if (!isset($mime_type)) { + $mime_type = $this->defaultMimeType; + } $out = $this->curlExec( array( CURLOPT_HTTPGET => TRUE, @@ -92,6 +97,7 @@ protected function basicAuthGet(Url $url, $username, $password) { CURLOPT_NOBODY => FALSE, CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_USERPWD => $username . ':' . $password, + CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type), ) ); diff --git a/core/modules/rest/src/Tests/CsrfTest.php b/core/modules/rest/src/Tests/CsrfTest.php index d44d787..a686f448 100644 --- a/core/modules/rest/src/Tests/CsrfTest.php +++ b/core/modules/rest/src/Tests/CsrfTest.php @@ -61,9 +61,6 @@ protected function setUp() { * Tests that CSRF check is not triggered for Basic Auth requests. */ public function testBasicAuth() { - // Login so the session cookie is sent in addition to the basic auth header. - $this->drupalLogin($this->account); - $curl_options = $this->getCurlOptions(); $curl_options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; $curl_options[CURLOPT_USERPWD] = $this->account->getUsername() . ':' . $this->account->pass_raw; diff --git a/core/modules/user/user.module b/core/modules/user/user.module index fbc900c..76edefc 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -575,8 +575,6 @@ function template_preprocess_username(&$variables) { * @see hook_user_login() */ function user_login_finalize(UserInterface $account) { - \Drupal::currentUser()->setAccount($account); - \Drupal::logger('user')->notice('Session opened for %name.', array('%name' => $account->getUsername())); // Update the user table timestamp noting user has logged in. // This is also used to invalidate one-time login links. $account->setLastLoginTime(REQUEST_TIME); @@ -588,7 +586,9 @@ function user_login_finalize(UserInterface $account) { // This is called before hook_user_login() in case one of those functions // fails or incorrectly does a redirect which would leave the old session // in place. + \Drupal::currentUser()->setAccount($account); \Drupal::service('session')->migrate(); + \Drupal::logger('user')->notice('Session opened for %name.', array('%name' => $account->getUsername())); \Drupal::moduleHandler()->invokeAll('user_login', array($account)); } diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml index 50405b5..2d92cdf 100644 --- a/core/modules/user/user.services.yml +++ b/core/modules/user/user.services.yml @@ -17,6 +17,7 @@ services: - { name: access_check, applies_to: _user_is_logged_in } authentication.cookie: class: Drupal\Core\Authentication\Provider\Cookie + arguments: ['@session_configuration', '@database'] tags: - { name: authentication_provider, priority: 0 } cache_context.user: diff --git a/core/tests/Drupal/Tests/Core/Authentication/AuthenticationManagerTest.php b/core/tests/Drupal/Tests/Core/Authentication/AuthenticationManagerTest.php new file mode 100644 index 0000000..370cc8c --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Authentication/AuthenticationManagerTest.php @@ -0,0 +1,88 @@ + TRUE]) { + $authentication_manager = new AuthenticationManager($global_providers); + $auth_provider = $this->getMock('Drupal\Core\Authentication\AuthenticationProviderInterface'); + $authentication_manager->addProvider($auth_provider, 'authentication.' . $provider_id); + + $request = new Request(); + if ($has_route) { + $route = new Route('/example'); + if ($auth_option) { + $route->setOption('_auth', $auth_option); + } + $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $route); + } + + $this->assertSame($applies, $authentication_manager->appliesToRoutedRequest($request, FALSE)); + } + + /** + * @covers ::applyFilter + */ + public function testApplyFilterWithFilterprovider() { + $authentication_manager = new AuthenticationManager(); + $auth_provider = $this->getMock('Drupal\Tests\Core\Authentication\TestAuthenticationProviderInterface'); + $authentication_manager->addProvider($auth_provider, 'authentication.filtered'); + + $auth_provider->expects($this->once()) + ->method('appliesToRoutedRequest') + ->willReturn(TRUE); + + $request = new Request(); + $this->assertTrue($authentication_manager->appliesToRoutedRequest($request, FALSE)); + } + + /** + * Provides data to self::testDefaultFilter(). + */ + public function providerTestDefaultFilter() { + $data = []; + // No route, cookie is global, should apply. + $data[] = [TRUE, FALSE, [], 'cookie']; + // No route, cookie is not global, should not apply. + $data[] = [FALSE, FALSE, [], 'cookie', ['other' => TRUE]]; + // Route, no _auth, cookie is global, should apply. + $data[] = [TRUE, TRUE, [], 'cookie']; + // Route, no _auth, cookie is not global, should not apply. + $data[] = [FALSE, TRUE, [], 'cookie', ['other' => TRUE]]; + // Route, with _auth and non-matching provider, should not apply. + $data[] = [FALSE, TRUE, ['basic_auth'], 'cookie']; + // Route, with _auth and matching provider should not apply. + $data[] = [TRUE, TRUE, ['basic_auth'], 'basic_auth']; + return $data; + } + +} + +/** + * Helper interface to mock two interfaces at once. + */ +interface TestAuthenticationProviderInterface extends AuthenticationProviderFilterInterface, AuthenticationProviderInterface {} diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/SpecialAttributesRouteSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/SpecialAttributesRouteSubscriberTest.php index 1c90a62..4b94055 100644 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/SpecialAttributesRouteSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/SpecialAttributesRouteSubscriberTest.php @@ -46,7 +46,6 @@ public function providerTestOnRouteBuildingInvalidVariables() { $routes = array(); $routes[] = array(new Route('/test/{system_path}')); $routes[] = array(new Route('/test/{_legacy}')); - $routes[] = array(new Route('/test/{_authentication_provider}')); $routes[] = array(new Route('/test/{' . RouteObjectInterface::ROUTE_OBJECT . '}')); $routes[] = array(new Route('/test/{' . RouteObjectInterface::ROUTE_NAME . '}')); $routes[] = array(new Route('/test/{_content}'));