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 03b8c7c..f21fc33 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -465,3 +465,22 @@ services: date: class: Drupal\Core\Datetime\Date arguments: ['@config.factory', '@language_manager'] + 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 + tags: + - { name: authentication_provider, priority: 100 } + authentication_subscriber: + class: Drupal\Core\EventSubscriber\AuthenticationSubscriber + tags: + - { name: event_subscriber } + arguments: ['@authentication'] + access_check.authentication_provider: + class: Drupal\Core\Access\AuthenticationProviderAccessCheck + tags: + - { name: access_check } diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index cf0e3dd..226bfbf 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(); 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 new file mode 100644 index 0000000..dc475aa --- /dev/null +++ b/core/lib/Drupal/Core/Access/AuthenticationProviderAccessCheck.php @@ -0,0 +1,46 @@ +attributes->get('_authentication_provider'); + if (!empty($auth_provider_triggered)) { + $allowed_auth_providers = $route->getOption('_auth'); + if (empty($allowed_auth_providers)) { + $result = NULL; + } + else { + $result = in_array($auth_provider_triggered, $allowed_auth_providers); + } + return $result; + } + } +} diff --git a/core/lib/Drupal/Core/Authentication/AuthenticationManager.php b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php new file mode 100644 index 0000000..d32e75a --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php @@ -0,0 +1,165 @@ +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 + * 0 - If both providers have the same priority. + * 1 - If provider A has lower priority than B. + * -1 - If provider A has higher priority than A. + */ + 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; + } + + /** + * Authenticate user. + * + * Iterate the available providers according th their priority. + * + * @return Drupal\Core\Session\AccountInterface + * The account interface of the authenticated user. Defaults to a anonymous. + */ + public function authenticate(Request $request) { + global $user; + + // Ensure providers get triggered according to their priority. + uksort($this->providers, array($this, 'compareProviderPriority')); + + // Iterate the availlable providers. + foreach ($this->providers as $provider_id => $provider) { + // Trigger authentication. + $account = $provider->authenticate($request); + + + // Provider felt responsible for this request. + if ($account !== NULL) { + $this->triggeredProvider = $provider_id; + // User failed authentication. + if ($account === FALSE) { + $user = $this->account = drupal_anonymous_user(); + throw new AccessDeniedHttpException(); + } + $this->account = $account; + break; + } + } + + // No provider returned a valid account. + if (!$this->account) { + throw new UnauthorizedHttpException('Basic realm="Drupal 8"', 'No authentication credentials provided.'); + } + + // Set the global user to the account returned by the triggered provider. + $user = $this->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', substr($this->triggeredProvider, strlen('authentication.'))); + + return $this->account; + } + + /** + * Do clean up. + * + * Allow the triggered provider to clean up before the response is sent, e.g. + * trigger a session commit. + * + * @param Request $request + * The request object. + * + * @see Drupal\Core\Authentication\Provider\Cookie::cleanup() + */ + public function cleanup(Request $request) { + if (empty($this->providers[$this->triggeredProvider])) { + return; + } + $this->providers[$this->triggeredProvider]->cleanup($request); + } +} diff --git a/core/lib/Drupal/Core/Authentication/AuthenticationProviderInterface.php b/core/lib/Drupal/Core/Authentication/AuthenticationProviderInterface.php new file mode 100644 index 0000000..f489d5c --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/AuthenticationProviderInterface.php @@ -0,0 +1,43 @@ +get('session_inc', 'core/includes/session.inc'); + drupal_session_initialize(); + if (drupal_session_started()) { + return $user; + } + return drupal_anonymous_user(); + } + + /** + * {@inheritdoc} + */ + public function cleanup(Request $request) { + drupal_session_commit(); + } +} diff --git a/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php b/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php new file mode 100644 index 0000000..b425f12 --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php @@ -0,0 +1,38 @@ +headers->get('PHP_AUTH_USER'); + $password = $request->headers->get('PHP_AUTH_PW'); + if ($username && $password) { + $uid = user_authenticate($username, $password); + if ($uid) { + return user_load($uid); + } + return FALSE; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function cleanup(Request $request) {} +} diff --git a/core/lib/Drupal/Core/Controller/ExceptionController.php b/core/lib/Drupal/Core/Controller/ExceptionController.php index c9a661b..cc076ad 100644 --- a/core/lib/Drupal/Core/Controller/ExceptionController.php +++ b/core/lib/Drupal/Core/Controller/ExceptionController.php @@ -57,7 +57,7 @@ public function execute(FlattenException $exception, Request $request) { return $this->$method($exception, $request); } - return new Response('A fatal error occurred: ' . $exception->getMessage(), $exception->getStatusCode()); + return new Response('A fatal error occurred: ' . $exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders()); } /** diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 9af0c74..b5aaa93 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -18,6 +18,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass; use Drupal\Core\DependencyInjection\Compiler\RegisterStringTranslatorsPass; use Drupal\Core\DependencyInjection\Compiler\RegisterBreadcrumbBuilderPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterAuthenticationPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -67,6 +68,8 @@ public function build(ContainerBuilder $container) { // Add the compiler pass that will process the tagged breadcrumb builder // services. $container->addCompilerPass(new RegisterBreadcrumbBuilderPass()); + // Add the compiler pass that will process tagged authentication services. + $container->addCompilerPass(new RegisterAuthenticationPass()); } /** diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAuthenticationPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAuthenticationPass.php new file mode 100644 index 0000000..c66a03a --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAuthenticationPass.php @@ -0,0 +1,44 @@ +hasDefinition('authentication')) { + return; + } + // Get the authentication manager. + $matcher = $container->getDefinition('authentication'); + // Iterate all autentication providers and add them to the manager. + foreach ($container->findTaggedServiceIds('authentication_provider') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $matcher->addMethodCall('addProvider', array( + $id, + new Reference($id), + $priority, + )); + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php new file mode 100644 index 0000000..8feb2ee --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php @@ -0,0 +1,81 @@ +authenticationManager = $authentication_manager; + } + + /** + * Authenticates user on request. + * + * @see Drupal\Core\Authentication\AuthenticationProviderInterface::authenticate() + */ + public function onKernelRequestAuthenticate(GetResponseEvent $event) { + if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { + $request = $event->getRequest(); + $this->authenticationManager->authenticate($request); + } + } + + /** + * Triggers authentication clean up on response. + * + * @see Drupal\Core\Authentication\AuthenticationProviderInterface::cleanup() + */ + public function onRespond(FilterResponseEvent $event) { + if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { + $request = $event->getRequest(); + + $this->authenticationManager->cleanup($request); + } + } + + /** + * {@inheritdoc} + * + * The priority for request must be higher than the highest event subscriber + * accessing the global $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. + */ + public static function getSubscribedEvents() { + // Priority must be higher than LanguageRequestSubscriber as LanguageManager + // access global $user in case language module enabled. + $events[KernelEvents::REQUEST][] = array('onKernelRequestAuthenticate', 300); + $events[KernelEvents::RESPONSE][] = array('onRespond', 0); + return $events; + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php index 0b9bbad..98eef05 100644 --- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php @@ -91,8 +91,6 @@ public function onRespond(FilterResponseEvent $event) { // @todo Revisit whether or not this is still appropriate now that the // Response object does its own cache control processing and we intend to // use partial page caching more extensively. - // Commit the user session, if needed. - drupal_session_commit(); // Attach globally-declared headers to the response object so that Symfony // can send them for us correctly. 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/Authentication/HttpBasicTest.php b/core/modules/system/lib/Drupal/system/Tests/Authentication/HttpBasicTest.php new file mode 100644 index 0000000..4c2e875 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Authentication/HttpBasicTest.php @@ -0,0 +1,86 @@ + 'HttpBasic authentication', + 'description' => 'Tests for HttpBasic authentication provider.', + 'group' => 'Authentication', + ); + } + + /** + * Test http basic authentication. + */ + public function testHttpBasic() { + $account = $this->drupalCreateUser(); + + $this->basicAuthGet('router_test/test11', $account->name, $account->pass_raw); + $this->assertText($account->name, 'Account name is displayed.'); + $this->assertResponse('200', 'HTTP response is OK'); + $this->curlClose(); + + $this->basicAuthGet('router_test/test11', $account->name, $this->randomName()); + $this->assertNoText($account->name, 'Bad basic auth credentials do not authenticate the user.'); + $this->assertResponse('200', 'HTTP response is OK'); + $this->curlClose(); + + $this->drupalGet('router_test/test11'); + $this->assertResponse('403', 'Not authenticated on the route that allows only http_basic.'); + } + + /** + * Dos HTTP basic auth request. + * + * We do not use drupalGet because we need to set curl settings for basic + * authentication. + * + * @param string $path + * The request path. + * @param string $username + * The user name to authenticate with. + * @param string $password + * The password. + * + * @return string + * Curl output. + */ + protected function basicAuthGet($path, $username, $password) { + $out = $this->curlExec( + array( + CURLOPT_HTTPGET => TRUE, + CURLOPT_URL => url($path, array('absolute' => TRUE)), + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_USERPWD => $username . ':' . $password, + ) + ); + + $this->verbose('GET request to: ' . $path . + '
' . $out); + + return $out; + } +} 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/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