diff --git a/core/authorize.php b/core/authorize.php index f347ba5..fe39394 100644 --- a/core/authorize.php +++ b/core/authorize.php @@ -48,10 +48,12 @@ function authorize_access_denied_page() { * The killswitch in settings.php overrides all else, otherwise, the user must * have access to the 'administer software updates' permission. * - * @return + * @return bool * TRUE if the current user can run authorize.php, and FALSE if not. */ function authorize_access_allowed() { + require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc'); + drupal_session_initialize(); return settings()->get('allow_authorize_operations', TRUE) && user_access('administer software updates'); } @@ -65,7 +67,7 @@ function authorize_access_allowed() { // We prepare only a minimal bootstrap. This includes the database and // variables, however, so we have access to the class autoloader. -drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION); +drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES); // This must go after drupal_bootstrap(), which unsets globals! global $conf; diff --git a/core/core.services.yml b/core/core.services.yml index 04e6ec0..e70cd10 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -282,6 +282,10 @@ services: tags: - { name: event_subscriber } arguments: ['@settings'] + route_enhancer.authentication: + class: Drupal\Core\Routing\Enhancer\AuthenticationEnhancer + tags: + - { name: route_enhancer, priority: 1000 } route_enhancer.content_controller: class: Drupal\Core\Routing\Enhancer\ContentControllerEnhancer arguments: ['@content_negotiation'] @@ -519,3 +523,19 @@ services: class: Zend\Feed\Writer\Extension\Threading\Renderer\Entry feed.writer.wellformedwebrendererentry: class: Zend\Feed\Writer\Extension\WellFormedWeb\Renderer\Entry + authentication: + class: Drupal\Core\Authentication\AuthenticationManager + authentication.cookie: + class: Drupal\Core\Authentication\Provider\Cookie + tags: + - { name: authentication_provider, priority: 0 } + authentication.http_basic: + class: Drupal\Core\Authentication\Provider\HttpBasic + arguments: ['@config.factory'] + tags: + - { name: authentication_provider, priority: 100 } + authentication_subscriber: + class: Drupal\Core\EventSubscriber\AuthenticationSubscriber + tags: + - { name: event_subscriber } + arguments: ['@authentication'] diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index cf0e3dd..5fae68d 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -158,11 +158,6 @@ const DRUPAL_BOOTSTRAP_VARIABLES = 4; /** - * Sixth bootstrap phase: initialize session handling. - */ -const DRUPAL_BOOTSTRAP_SESSION = 5; - -/** * Eighth bootstrap phase: load code for subsystems and modules. */ const DRUPAL_BOOTSTRAP_CODE = 6; @@ -1770,7 +1765,6 @@ function drupal_anonymous_user() { * - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page. * - DRUPAL_BOOTSTRAP_DATABASE: Initializes the database layer. * - DRUPAL_BOOTSTRAP_VARIABLES: Initializes the variable system. - * - DRUPAL_BOOTSTRAP_SESSION: Initializes session handling. * - DRUPAL_BOOTSTRAP_CODE: Loads code for subsystems and modules. * - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input * data. @@ -1789,7 +1783,6 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { DRUPAL_BOOTSTRAP_PAGE_CACHE, DRUPAL_BOOTSTRAP_DATABASE, DRUPAL_BOOTSTRAP_VARIABLES, - DRUPAL_BOOTSTRAP_SESSION, DRUPAL_BOOTSTRAP_CODE, DRUPAL_BOOTSTRAP_FULL, ); @@ -1839,11 +1832,6 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) { _drupal_bootstrap_variables(); break; - case DRUPAL_BOOTSTRAP_SESSION: - require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc'); - drupal_session_initialize(); - break; - case DRUPAL_BOOTSTRAP_CODE: require_once __DIR__ . '/common.inc'; _drupal_bootstrap_code(); @@ -1911,7 +1899,7 @@ function drupal_get_user_timezone() { global $user; $config = config('system.timezone'); - if ($config->get('user.configurable') && $user->uid && $user->timezone) { + if ($user && $config->get('user.configurable') && $user->uid && $user->timezone) { return $user->timezone; } else { diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 1ecd819..5f2ccc7 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1746,6 +1746,8 @@ function install_load_profile(&$install_state) { */ function install_bootstrap_full() { drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); + require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc'); + drupal_session_initialize(); } /** diff --git a/core/lib/Drupal/Core/Access/AuthenticationProviderAccessCheck.php b/core/lib/Drupal/Core/Access/AuthenticationProviderAccessCheck.php deleted file mode 100644 index 799dc00..0000000 --- a/core/lib/Drupal/Core/Access/AuthenticationProviderAccessCheck.php +++ /dev/null @@ -1,52 +0,0 @@ -attributes->get('_authentication_provider'); - if (!empty($auth_provider_triggered)) { - $allowed_auth_providers = $route->getOption('_auth'); - if (empty($allowed_auth_providers)) { - return NULL; - } - $check = in_array($auth_provider_triggered, $allowed_auth_providers); - - // Set user as anonymous as he should not be logged in because he logged in - // with not allowed authentication provider. - if ($check == FALSE) { - global $user; - $user = drupal_anonymous_user(); - } - - return $check; - } - } -} diff --git a/core/lib/Drupal/Core/Authentication/AuthenticationManager.php b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php index 99529b3..aa49f5f 100644 --- a/core/lib/Drupal/Core/Authentication/AuthenticationManager.php +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php @@ -14,10 +14,11 @@ /** * Manager for authentication. * - * On each request, let all authentication provider to authenticate the user. - * The provider are iterated according to their priority and the first provider - * detecting credentials for his method will become the triggered provider. No - * further provider will get triggered. + * On each request, let all authentication providers try to authenticate the + * user. The provider are iterated according to their priority and the first + * provider detecting credentials for his method will become the triggered + * provider. No further provider will get triggered. + * * If no provider felt responsible the manager assumes that the least * prioritized should have and therefore is the triggered provider and the user * is set to anonymous. @@ -25,37 +26,32 @@ class AuthenticationManager implements AuthenticationProviderInterface { /** - * Array of all registered authentication providers. + * Array of all registered authentication providers, keyed by ID. * * @var array */ protected $providers; /** - * Priority array off all registered authentication providers. - * - * This is used to sort the providers based on their priority. + * Array of all providers and their priority. * * @var array - * - * @see AuthenticationManager::compareProviderPriority() - * @see AuthenticationManager::addProvider() */ - protected $providerPriorities; + protected $providerOrders; /** - * Id of the provider that authenticated the user. + * Sorted list of registered providers. * - * @var string + * @var array */ - protected $triggeredProvider = ''; + protected $sortedProviders; /** - * The account resulting from the authentication process. + * Id of the provider that authenticated the user. * - * @var \Drupal\Core\Session\AccountInterface + * @var string */ - protected $account; + protected $triggeredProviderId = ''; /** * Add provider to the array of registered providers. @@ -69,49 +65,11 @@ class AuthenticationManager implements AuthenticationProviderInterface { */ public function addProvider($provider_id, AuthenticationProviderInterface $provider, $priority = 0) { $provider_id = substr($provider_id, strlen('authentication.')); - $this->providers[$provider_id] = $provider; - $this->providerPriorities[$provider_id] = $priority; - } - - /** - * Compares providers by their priority. - * - * This is a uksort() callback allowing to sort the providers by their keys - * based on their priorities. - * - * @param string $a - * A provider key. - * @param string $b - * Another provider key. - * - * @return int - * The integer indicating the sort order. - */ - public function compareProviderPriority($a, $b) { - $priority_a = isset($this->providerPriorities[$a]) ? $this->providerPriorities[$a] : 0; - $priority_b = isset($this->providerPriorities[$b]) ? $this->providerPriorities[$b] : 0; - if ($priority_a == $priority_b) { - return 0; - } - return ($priority_a > $priority_b) ? -1 : 1; - } - /** - * Compares providers by their reversed priority. - * - * This is a uksort() callback allowing to sort the providers by their keys - * based on their reversed priorities. - * - * @param string $a - * A provider key. - * @param string $b - * Another provider key. - * - * @return int - * - */ - public function compareProviderReversedPriority($a, $b) { - return $this->compareProviderPriority($a, $b) * -1; + $this->providers[$provider_id] = $provider; + $this->providerOrders[$priority][$provider_id] = $provider; + // Force the builders to be re-sorted. + $this->sortedProviders = NULL; } /** @@ -135,39 +93,71 @@ public function applies(Request $request) { public function authenticate(Request $request) { global $user; - // Ensure providers get triggered according to their priority. - uksort($this->providers, array($this, 'compareProviderPriority')); + $account = NULL; // Iterate the availlable providers. - foreach ($this->providers as $provider_id => $provider) { + foreach ($this->getSortedProviders() as $provider_id => $provider) { if ($provider->applies($request)) { // Provider felt responsible for this request – trigger authentication. - $this->account = $provider->authenticate($request); - $this->triggeredProvider = $provider_id; + $account = $provider->authenticate($request); + $this->triggeredProviderId = $provider_id; break; } } // No provider returned a valid account - assume anonymous. - if (!$this->account) { - $this->account = drupal_anonymous_user(); + if (!$account) { + $account = drupal_anonymous_user(); } // No provider felt responsible – assume the one with the least priority // should have. - if (!$this->triggeredProvider) { - end($this->providers); - $this->triggeredProvider = key($this->providers); + if (!$this->triggeredProviderId) { + $this->triggeredProviderId = $this->defaultProviderId(); } - // Set the global user to the account returned by the triggered provider. - $user = $this->account; + // Save the authenticated account for later access. The global $user object + // is included for backward compatibility only and should be considered + // deprecated. + $user = $account; + $request->attributes->set('session', $account); - // Save the ID of the triggered provider to the request so that it can be - // accessed in \Drupal\Core\Access\AuthenticationProviderAccessCheck. - $request->attributes->set('_authentication_provider', $this->triggeredProvider); + // Save the ID of the triggered provider to the request. + $request->attributes->set('_authentication_provider', $this->triggeredProviderId); - return $this->account; + return $account; + } + + /** + * Returns the default provider ID. + * + * The default provider is the one with the lowest registered priority. + * + * @return string + * The ID of the default provider + */ + protected function defaultProviderId() { + $providers = $this->getSortedProviders(); + return $providers[end(array_keys($providers))]; + } + + /** + * Returns the sorted array of authentication providers. + * + * @return array + * An array of authentication provider objects. + */ + 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; } /** @@ -182,10 +172,10 @@ public function authenticate(Request $request) { * @see \Drupal\Core\Authentication\Provider\Cookie::cleanup() */ public function cleanup(Request $request) { - if (empty($this->providers[$this->triggeredProvider])) { + if (empty($this->providers[$this->triggeredProviderId])) { return; } - $this->providers[$this->triggeredProvider]->cleanup($request); + $this->providers[$this->triggeredProviderId]->cleanup($request); } /** @@ -193,20 +183,26 @@ public function cleanup(Request $request) { */ public function handleException(GetResponseForExceptionEvent $event) { $request = $event->getRequest(); - $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT); - $allowed_auth_providers = $route ? $route->getOption('_auth') : array_keys($this->providers); - // Ensure providers get triggered according to their reversed priority. - uksort($this->providers, array($this, 'compareProviderReversedPriority')); + // Legacy routes won't have a Route object; they have drupal_menu_item + // instead. Assume those were authenticated by cookie, because the legacy + // router didn't support anything else. + // @todo Remove this check once the old router is fully removed. + if ($request->attributes->has('drupal_menu_item')) { + $auth_providers = array('cookie'); + } + else { + $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT); + $auth_providers = ($route && $route->getOption('_auth')) ? $route->getOption('_auth') : array($this->defaultProviderId()); + } - foreach ($this->providers as $provider_id => $provider) { - if (!in_array($provider_id, $allowed_auth_providers)) { - continue; - } + // While any provider may authenticate a request, only the first listed + // by a route may give a challenge for a failed request to allow the user + // to login. + $allowed_providers = array_intersect($auth_providers, array_keys($this->providers)); - if ($provider->handleException($event) === TRUE) { - break; - } + if ($allowed_providers) { + $this->providers[current($allowed_providers)]->handleException($event); } } } diff --git a/core/lib/Drupal/Core/Authentication/Provider/Cookie.php b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php index 495f8c8..04a6992 100644 --- a/core/lib/Drupal/Core/Authentication/Provider/Cookie.php +++ b/core/lib/Drupal/Core/Authentication/Provider/Cookie.php @@ -10,8 +10,9 @@ use Drupal\Core\Authentication\AuthenticationProviderInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; /** * Cookie based authentication provider. @@ -29,6 +30,7 @@ public function applies(Request $request) { * {@inheritdoc} */ public function authenticate(Request $request) { + // Global $user is deprecated, but the session system is still based on it. global $user; require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc'); drupal_session_initialize(); @@ -50,7 +52,7 @@ public function cleanup(Request $request) { */ public function handleException(GetResponseForExceptionEvent $event) { $exception = $event->getException(); - if (user_is_anonymous() && get_class($exception) == 'Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException') { + if (user_is_anonymous() && $exception instanceof AccessDeniedHttpException) { $event->setResponse(new RedirectResponse(url('user/login', array( 'absolute' => TRUE, 'query' => array( diff --git a/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php b/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php index 9307731..50ca04f 100644 --- a/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php +++ b/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php @@ -11,9 +11,10 @@ use Drupal\Core\Authentication\AuthenticationProviderInterface; use Drupal\Core\Config\Config; use Drupal\Core\Config\ConfigFactory; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * HTTP Basic authentication provider. @@ -69,7 +70,7 @@ public function cleanup(Request $request) {} */ public function handleException(GetResponseForExceptionEvent $event) { $exception = $event->getException(); - if (user_is_anonymous() && get_class($exception) == 'Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException') { + if (user_is_anonymous() && $exception instanceof AccessDeniedHttpException) { if (!$this->applies($event->getRequest())) { $site_name = $this->configFactory->get('system.site')->get('name'); global $base_url; diff --git a/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php new file mode 100644 index 0000000..9a342fc --- /dev/null +++ b/core/lib/Drupal/Core/Routing/Enhancer/AuthenticationEnhancer.php @@ -0,0 +1,44 @@ +attributes->get('_authentication_provider'); + if (!empty($auth_provider_triggered)) { + $route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT); + $auth_providers = ($route && $route->getOption('_auth')) ? $route->getOption('_auth') : array(); + if (!empty($auth_providers)) { + // If the request was authenticated with a non-permitted provider, + // force the user back to anonymous. + if (!in_array($auth_provider_triggered, $auth_providers)) { + $request->attributes->set('session', drupal_anonymous_user()); + } + } + } + return $defaults; + } + +} diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index d3ba7b3..975788a 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -876,6 +876,7 @@ protected function prepareEnvironment() { $this->originalUser = isset($user) ? clone $user : NULL; // Ensure that the current session is not changed by the new environment. + require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc'); drupal_save_session(FALSE); // Run all tests as a anonymous user by default, web tests will replace that // during the test set up. diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php index b613121..9de96b0 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php @@ -93,6 +93,10 @@ protected function setUp() { // Load the Update API. require_once DRUPAL_ROOT . '/core/includes/update.inc'; + // Load Session API. + require_once DRUPAL_ROOT . '/core/includes/session.inc'; + drupal_session_initialize(); + // Reset flags. $this->upgradedSite = FALSE; $this->upgradeErrors = array(); diff --git a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php index 56d567b..22a07cf 100644 --- a/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php +++ b/core/modules/system/tests/modules/router_test/lib/Drupal/router_test/TestContent.php @@ -19,4 +19,15 @@ public function test1() { return 'abcde'; } + /** + * Provides example content for route specific authentication. + * + * @returns string + * The user name of the current logged in user. + */ + public function test11() { + global $user; + return isset($user->name) ? $user->name : ''; + } + } diff --git a/core/modules/system/tests/modules/router_test/router_test.routing.yml b/core/modules/system/tests/modules/router_test/router_test.routing.yml index 2a989df..9e481de 100644 --- a/core/modules/system/tests/modules/router_test/router_test.routing.yml +++ b/core/modules/system/tests/modules/router_test/router_test.routing.yml @@ -60,3 +60,10 @@ router_test_10: _content: '\Drupal\router_test\TestContent::test1' requirements: _access: 'TRUE' + +router_test_11: + pattern: '/router_test/test11' + options: + _auth: [ 'http_basic' ] + defaults: + _content: '\Drupal\router_test\TestContent::test11' diff --git a/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/EventSubscriber/SessionTestSubscriber.php b/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/EventSubscriber/SessionTestSubscriber.php index 45234a8..44e2882 100644 --- a/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/EventSubscriber/SessionTestSubscriber.php +++ b/core/modules/system/tests/modules/session_test/lib/Drupal/session_test/EventSubscriber/SessionTestSubscriber.php @@ -65,7 +65,7 @@ public function onKernelResponseSessionTest(FilterResponseEvent $event) { */ static function getSubscribedEvents() { $events[KernelEvents::RESPONSE][] = array('onKernelResponseSessionTest', 300); - $events[KernelEvents::REQUEST][] = array('onKernelRequestSessionTest', 300); + $events[KernelEvents::REQUEST][] = array('onKernelRequestSessionTest', 100); return $events; } diff --git a/core/modules/user/lib/Drupal/user/Access/RoleAccessCheck.php b/core/modules/user/lib/Drupal/user/Access/RoleAccessCheck.php index 7940bf3..928af4d 100644 --- a/core/modules/user/lib/Drupal/user/Access/RoleAccessCheck.php +++ b/core/modules/user/lib/Drupal/user/Access/RoleAccessCheck.php @@ -34,9 +34,7 @@ public function access(Route $route, Request $request) { // Requirements just allow strings, so this might be a comma separated list. $rid_string = $route->getRequirement('_role'); - // @todo Replace the role check with a correctly injected and session-using - // alternative. - $account = $GLOBALS['user']; + $account = $request->attributes->get('session'); $explode_and = array_filter(array_map('trim', explode('+', $rid_string))); if (count($explode_and) > 1) { diff --git a/core/modules/user/user.module b/core/modules/user/user.module index e7ee37a..e9785f6 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -470,7 +470,9 @@ function user_access($string, AccountInterface $account = NULL) { global $user; if (!isset($account)) { - $account = $user; + // In the installer request session is not set, so we have to fall back + // to the global $user. In all other cases the session key is preferred. + $account = Drupal::request()->attributes->get('session') ?: $user; } // Make sure we are working with the BC decorator. diff --git a/core/update.php b/core/update.php index b73e3ce..f97a973 100644 --- a/core/update.php +++ b/core/update.php @@ -427,7 +427,9 @@ function update_check_requirements($skip_warnings = FALSE) { update_prepare_d8_bootstrap(); // Determine if the current user has access to run update.php. -drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION); +drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES); +require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc'); +drupal_session_initialize(); // A request object from the HTTPFoundation to tell us about the request. // @todo These two lines were copied from index.php which has its own todo about