Commit aa9c2ccc authored by alexpott's avatar alexpott

Issue #1890878 by corvus_ch, ygerasimov, berenddeboer, Crell, fubhy the cat,...

Issue #1890878 by corvus_ch, ygerasimov, berenddeboer, Crell, fubhy the cat, effulgentsia: Add modular authentication system, including Http Basic; deprecate global .
parent 30a45ade
......@@ -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;
......
......@@ -296,6 +296,11 @@ services:
tags:
- { name: event_subscriber }
arguments: ['@settings']
route_enhancer.authentication:
class: Drupal\Core\Routing\Enhancer\AuthenticationEnhancer
tags:
- { name: route_enhancer, priority: 1000 }
arguments: ['@authentication']
route_enhancer.content_controller:
class: Drupal\Core\Routing\Enhancer\ContentControllerEnhancer
arguments: ['@content_negotiation']
......@@ -538,3 +543,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']
......@@ -157,20 +157,15 @@
*/
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;
const DRUPAL_BOOTSTRAP_CODE = 5;
/**
* Final bootstrap phase: initialize language, path, theme, and modules.
*/
const DRUPAL_BOOTSTRAP_FULL = 7;
const DRUPAL_BOOTSTRAP_FULL = 6;
/**
* Role ID for anonymous users; should match what's in the "role" table.
......@@ -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();
......@@ -1910,7 +1898,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 {
......
......@@ -1752,6 +1752,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();
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\Authentication\AuthenticationManager.
*/
namespace Drupal\Core\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
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.
*
* 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.
*/
class AuthenticationManager implements AuthenticationProviderInterface, AuthenticationManagerInterface {
/**
* Array of all registered authentication providers, keyed by ID.
*
* @var array
*/
protected $providers;
/**
* Array of all providers and their priority.
*
* @var array
*/
protected $providerOrders;
/**
* Sorted list of registered providers.
*
* @var array
*/
protected $sortedProviders;
/**
* Id of the provider that authenticated the user.
*
* @var string
*/
protected $triggeredProviderId = '';
/**
* Adds a provider to the array of registered providers.
*
* @param string $provider_id
* Identifier of the provider.
* @param \Drupal\Core\Authentication\AuthenticationProviderInterface $provider
* The provider object.
* @param int $priority
* The providers priority.
*/
public function addProvider($provider_id, AuthenticationProviderInterface $provider, $priority = 0) {
$provider_id = substr($provider_id, strlen('authentication.'));
$this->providers[$provider_id] = $provider;
$this->providerOrders[$priority][$provider_id] = $provider;
// Force the builders to be re-sorted.
$this->sortedProviders = NULL;
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
global $user;
$account = NULL;
// Iterate the availlable providers.
foreach ($this->getSortedProviders() 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;
}
}
// No provider returned a valid account, so set the user to anonymous.
if (!$account) {
$account = drupal_anonymous_user();
}
// No provider was fired, so assume the one with the least priority
// should have.
if (!$this->triggeredProviderId) {
$this->triggeredProviderId = $this->defaultProviderId();
}
// Save the authenticated account and the provider that supplied it
// for later access.
$request->attributes->set('account', $account);
$request->attributes->set('_authentication_provider', $this->triggeredProviderId);
// The global $user object is included for backward compatibility only and
// should be considered deprecated.
// @todo Remove this line once global $user is no longer used.
$user = $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.
*/
public function defaultProviderId() {
$providers = $this->getSortedProviders();
$provider_ids = array_keys($providers);
return end($provider_ids);
}
/**
* 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;
}
/**
* Cleans up the authentication.
*
* Allow the triggered provider to clean up before the response is sent, e.g.
* trigger a session commit.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @see \Drupal\Core\Authentication\Provider\Cookie::cleanup()
*/
public function cleanup(Request $request) {
if (empty($this->providers[$this->triggeredProviderId])) {
return;
}
$this->providers[$this->triggeredProviderId]->cleanup($request);
}
/**
* {@inheritdoc}
*/
public function handleException(GetResponseForExceptionEvent $event) {
$request = $event->getRequest();
// 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')) {
$active_providers = array('cookie');
}
else {
$route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT);
$active_providers = ($route && $route->getOption('_auth')) ? $route->getOption('_auth') : array($this->defaultProviderId());
}
// Get the sorted list of active providers for the given route.
$providers = array_intersect($active_providers, array_keys($this->providers));
foreach ($providers as $provider_id) {
if ($this->providers[$provider_id]->handleException($event) == TRUE) {
break;
}
}
}
}
<?php
/**
* @file
* Contains Drupal\Core\Authentication\AuthenticationManagerInterface.
*/
namespace Drupal\Core\Authentication;
/**
* Defines an interface for authentication managers.
*/
interface AuthenticationManagerInterface {
/**
* Returns the service id of the default authentication provider.
*
* @return string
* The service id of the default authentication provider.
*/
public function defaultProviderId();
}
<?php
/**
* @file
* Contains \Drupal\Core\Authentication\AuthenticationProviderInterface.
*/
namespace Drupal\Core\Authentication;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
/**
* Interface for authentication providers.
*/
interface AuthenticationProviderInterface {
/**
* Declares whether the provider applies to a specific request or not.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return bool
* TRUE if the provider applies to the passed request, FALSE otherwise.
*/
public function applies(Request $request);
/**
* Authenticates the user.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \Drupal\Core\Session\AccountInterface|null
* AccountInterface - in case of a successful authentication.
* NULL - in case where authentication failed.
*/
public function authenticate(Request $request);
/**
* Performs cleanup tasks at the end of a request.
*
* Allow the authentication provider to clean up before the response is sent.
* This is uses for instance in \Drupal\Core\Authentication\Provider\Cookie to
* ensure the session gets committed.
*
* @param Request $request
* The request object.
*/
public function cleanup(Request $request);
/**
* Handles an exception.
*
* In case exception has happened we allow authentication providers react.
* Used in \Drupal\Core\Authentication\Provider\HttpBasic to set up headers to
* prompt login.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
*
* @return bool
* TRUE - exception handled. No need to run through other providers.
* FALSE - no actions have been done. Run through other providers.
*/
public function handleException(GetResponseForExceptionEvent $event);
}
<?php
/**
* @file
* Contains \Drupal\Core\Authentication\Provider\Cookie.
*/
namespace Drupal\Core\Authentication\Provider;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
/**
* Cookie based authentication provider.
*/
class Cookie implements AuthenticationProviderInterface {
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return TRUE;
}
/**
* {@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();
if (drupal_session_started()) {
return $user;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function cleanup(Request $request) {
drupal_session_commit();
}
/**
* {@inheritdoc}
*/
public function handleException(GetResponseForExceptionEvent $event) {
return FALSE;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Authentication\Provider\HttpBasic.
*/
namespace Drupal\Core\Authentication\Provider;
use \Drupal\Component\Utility\String;
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\HttpKernel\Exception\AccessDeniedHttpException;
/**
* HTTP Basic authentication provider.
*/
class HttpBasic implements AuthenticationProviderInterface {
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactory
*/
protected $configFactory;
/**
* Constructs a HTTP basic authentication provider object.
*
* @param \Drupal\Core\Config\ConfigFactory $config_factory
* The config factory.
*/
public function __construct(ConfigFactory $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
$username = $request->headers->get('PHP_AUTH_USER');
$password = $request->headers->get('PHP_AUTH_PW');
return isset($username) && isset($password);
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
$username = $request->headers->get('PHP_AUTH_USER');
$password = $request->headers->get('PHP_AUTH_PW');
$uid = user_authenticate($username, $password);
if ($uid) {
return user_load($uid);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function cleanup(Request $request) {}
/**
* {@inheritdoc}
*/
public function handleException(GetResponseForExceptionEvent $event) {
$exception = $event->getException();
if (user_is_anonymous() && $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;
}
}
......@@ -42,12 +42,12 @@ public function __construct(ContentNegotiation $negotiation) {
/**
* Handles an exception on a request.
*
* @param Symfony\Component\HttpKernel\Exception\FlattenException $exception
* @param \Symfony\Component\HttpKernel\Exception\FlattenException $exception
* The flattened exception.
* @param Symfony\Component\HttpFoundation\Request $request
* @param \Symfony\Component\HttpFoundation\Request $request
* The request that generated the exception.
*
* @return Symfony\Component\HttpFoundation\Response
* @return \Symfony\Component\HttpFoundation\Response
* A response object to be sent to the server.
*/
public function execute(FlattenException $exception, Request $request) {
......@@ -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());
}
/**
......
......@@ -21,6 +21,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\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
......@@ -71,6 +72,8 @@ public function register(ContainerBuilder $container) {
// Add the compiler pass that lets service providers modify existing
// service definitions.
$container->addCompilerPass(new ModifyServiceDefinitionsPass());
// Add the compiler pass that will process tagged authentication services.
$container->addCompilerPass(new RegisterAuthenticationPass());
}
/**
......
<?php
/**
* @file
* Contains \Drupal\Core\DependencyInjection\Compiler\RegisterAuthenticationPass.
*/
namespace Drupal\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds services tagged 'authentication_provider'.
*/
class RegisterAuthenticationPass implements CompilerPassInterface {
/**
* Adds authentication providers to the authentication manager.
*
* Check for services tagged with 'authentication_provider' and add them to
* the authentication manager.
*
* @see \Drupal\Core\Authentication\AuthenticationManager
* @see \Drupal\Core\Authentication\AuthenticationProviderInterface
*/
public function process(ContainerBuilder $container) {
if (!$container->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,
));
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\AuthenticationSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
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.
*/
class AuthenticationSubscriber implements EventSubscriberInterface {
/**
* Authentication provider.
*
* @var AuthenticationProviderInterface
*/
protected $authenticationProvider;
/**
* Keep authentication manager as private variable.
*
* @param AuthenticationProviderInterface $authentication_manager
* The authentication manager.
*/
public function __construct(AuthenticationProviderInterface $authentication_provider) {
$this->authenticationProvider = $authentication_provider;
}
/**
* 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->authenticationProvider->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->authenticationProvider->cleanup($request);
}
}