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 e566d70..92cbf61 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -457,3 +457,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 43415e7..12f9b9a 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; @@ -1772,7 +1767,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. @@ -1791,7 +1785,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, ); @@ -1841,11 +1834,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 f798392..d6f3e5b 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1707,6 +1707,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..090ac00 --- /dev/null +++ b/core/lib/Drupal/Core/Access/AuthenticationProviderAccessCheck.php @@ -0,0 +1,47 @@ +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..f7a9e0d --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/AuthenticationManager.php @@ -0,0 +1,138 @@ +providers[$provider_id] = $provider; + $this->providerPriorities[$provider_id] = $priority; + uksort($this->providers, array($this, 'compareProviderPriority')); + } + + /** + * Compares providers by their priority. + * + * This is a uksort() callback to sort the providers by their keys based on + * the 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 by running authenticate() method on each of providers. + * + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + * Throws exception in case two authentication providers had credentials. + */ + public function authenticate(Request $request) { + global $user; + + foreach ($this->providers as $provider_id => $provider) { + $account = $provider->authenticate($request); + + if ($account !== NULL) { + // We do not allow request to have information to authenticate + // with two methods at the same time. + if (!empty($this->triggeredProvider)) { + throw new BadRequestHttpException(t('Multiple authentication methods are not allowed.')); + } + $this->account = $account; + $this->triggeredProvider = $provider_id; + } + } + + // No provider returned a valid account - assume anonymous. + if (!$this->account) { + $this->account = drupal_anonymous_user(); + } + + // 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 AuthenticationProviderAccessCheck. + $request->attributes->set('_authentication_provider', substr($this->triggeredProvider, strlen('authentication.'))); + } + + /** + * Does clean up by running cleanup() of provider that authenticated the user. + * + * @param Request $request + * The request object. + */ + public function cleanup(Request $request) { + if (empty($this->triggeredProvider)) { + return; + } + $provider = $this->providers[$this->triggeredProvider]; + $provider->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..3511429 --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/AuthenticationProviderInterface.php @@ -0,0 +1,39 @@ +get('session_inc', 'core/includes/session.inc'); + drupal_session_initialize(); + if (drupal_session_started()) { + return $user; + } + return NULL; + } + + /** + * {@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..61284c4 --- /dev/null +++ b/core/lib/Drupal/Core/Authentication/Provider/HttpBasic.php @@ -0,0 +1,37 @@ +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 NULL; + } + } + + /** + * {@inheritdoc} + */ + public function cleanup(Request $request) {} +} diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 472105a..3e6147b 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -17,6 +17,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterServicesForDestructionPass; use Drupal\Core\DependencyInjection\Compiler\RegisterStringTranslatorsPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterAuthenticationPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -63,6 +64,8 @@ public function build(ContainerBuilder $container) { $container->addCompilerPass(new ListCacheBinsPass()); // Add the compiler pass for appending string translators. $container->addCompilerPass(new RegisterStringTranslatorsPass()); + // Addd 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..cda687a --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterAuthenticationPass.php @@ -0,0 +1,36 @@ +hasDefinition('authentication')) { + return; + } + $matcher = $container->getDefinition('authentication'); + 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..073a6dd --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php @@ -0,0 +1,72 @@ +authenticationManager = $authentication_manager; + } + + /** + * Authenticate user. + */ + public function onKernelRequestAuthenticate(GetResponseEvent $event) { + if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { + $request = $event->getRequest(); + $this->authenticationManager->authenticate($request); + } + } + + /** + * Trigger clean up. + */ + public function onRespond(FilterResponseEvent $event) { + if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { + $request = $event->getRequest(); + + $this->authenticationManager->cleanup($request); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + 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..1e9a652 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Authentication/HttpBasicTest.php @@ -0,0 +1,84 @@ + '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->curlClose(); + + $this->basicAuthGet('router_test/test11', $account->name, $this->randomName()); + $this->assertNoText($account->name, 'Bad basic auth credentials do not authenticate the user.'); + $this->curlClose(); + + // Login to have a valid session. + $this->drupalLogin($account); + // This call should have both session cookies and basic authentication. So + // it should fail. + $this->basicAuthGet('router_test/test11', $account->name, $account->pass_raw); + $this->assertResponse('400', 'Multiple authentication methods in request lead to 400.'); + $this->curlClose(); + + $this->drupalGet('router_test/test11'); + $this->assertResponse('403', 'Not authenticated on the route that allows only http_basic.'); + } + + /** + * Do 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..f7deee8 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,12 @@ public function test1() { return 'abcde'; } + /** + * Provides example content for route specific authentication. + */ + public function test11() { + global $user; + return $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 c9f1b0a..479b880 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 @@ -50,7 +50,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 a3dd4bb..921dd81 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