Merge branch 'feature/basic-auth-9660'

resolves #9660
This commit is contained in:
Eric Lippmann 2015-07-30 15:05:07 +02:00
commit a234852f32
30 changed files with 957 additions and 407 deletions

View File

@ -6,20 +6,14 @@
use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Authentication\AuthChain;
use Icinga\Authentication\User\ExternalBackend;
use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError;
use Icinga\Forms\Authentication\LoginForm;
use Icinga\User;
use Icinga\Web\Controller\ActionController;
use Icinga\Web\Controller;
use Icinga\Web\Url;
/**
* Application wide controller for authentication
*/
class AuthenticationController extends ActionController
class AuthenticationController extends Controller
{
/**
* This controller does not require authentication
@ -34,118 +28,19 @@ class AuthenticationController extends ActionController
public function loginAction()
{
$icinga = Icinga::app();
if ($icinga->setupTokenExists() && $icinga->requiresSetup()) {
if (($requiresSetup = $icinga->requiresSetup()) && $icinga->setupTokenExists()) {
$this->redirectNow(Url::fromPath('setup'));
}
$triedOnlyExternalAuth = null;
$auth = $this->Auth();
$this->view->form = $form = new LoginForm();
$this->view->title = $this->translate('Icingaweb Login');
try {
$redirectUrl = $this->view->form->getValue('redirect');
if ($redirectUrl) {
$redirectUrl = Url::fromPath($redirectUrl);
} else {
$redirectUrl = Url::fromPath('dashboard');
}
if ($auth->isAuthenticated()) {
$this->rerenderLayout()->redirectNow($redirectUrl);
}
try {
$config = Config::app('authentication');
} catch (NotReadableError $e) {
throw new ConfigurationError(
$this->translate('Could not read your authentication.ini, no authentication methods are available.'),
0,
$e
);
}
$chain = new AuthChain($config);
$request = $this->getRequest();
if ($request->isPost() && $this->view->form->isValid($request->getPost())) {
$user = new User($this->view->form->getValue('username'));
$password = $this->view->form->getValue('password');
$backendsTried = 0;
$backendsWithError = 0;
$redirectUrl = $form->getValue('redirect');
if ($redirectUrl) {
$redirectUrl = Url::fromPath($redirectUrl);
} else {
$redirectUrl = Url::fromPath('dashboard');
}
foreach ($chain as $backend) {
if ($backend instanceof ExternalBackend) {
continue;
}
++$backendsTried;
try {
$authenticated = $backend->authenticate($user, $password);
} catch (AuthenticationException $e) {
Logger::error($e);
++$backendsWithError;
continue;
}
if ($authenticated === true) {
$auth->setAuthenticated($user);
$this->rerenderLayout()->redirectNow($redirectUrl);
}
}
if ($backendsTried === 0) {
$this->view->form->addError(
$this->translate(
'No authentication methods available. Did you create'
. ' authentication.ini when setting up Icinga Web 2?'
)
);
} else if ($backendsTried === $backendsWithError) {
$this->view->form->addError(
$this->translate(
'All configured authentication methods failed.'
. ' Please check the system log or Icinga Web 2 log for more information.'
)
);
} elseif ($backendsWithError) {
$this->view->form->addError(
$this->translate(
'Please note that not all authentication methods were available.'
. ' Check the system log or Icinga Web 2 log for more information.'
)
);
}
if ($backendsTried > 0 && $backendsTried !== $backendsWithError) {
$this->view->form->getElement('password')->addError($this->translate('Incorrect username or password'));
}
} elseif ($request->isGet()) {
$user = new User('');
foreach ($chain as $backend) {
$triedOnlyExternalAuth = $triedOnlyExternalAuth === null;
if ($backend instanceof ExternalBackend) {
$authenticated = $backend->authenticate($user);
if ($authenticated === true) {
$auth->setAuthenticated($user);
$this->rerenderLayout()->redirectNow(
Url::fromPath(Url::fromRequest()->getParam('redirect', 'dashboard'))
);
}
} else {
$triedOnlyExternalAuth = false;
}
}
}
} catch (Exception $e) {
$this->view->form->addError($e->getMessage());
$form = new LoginForm();
if ($this->Auth()->isAuthenticated()) {
$this->redirectNow($form->getRedirectUrl());
}
$this->view->requiresExternalAuth = $triedOnlyExternalAuth && ! $auth->isAuthenticated();
$this->view->requiresSetup = Icinga::app()->requiresSetup();
if (! $requiresSetup) {
$form->handleRequest();
}
$this->view->form = $form;
$this->view->title = $this->translate('Icinga Web 2 Login');
$this->view->requiresSetup = $requiresSetup;
}
/**
@ -157,10 +52,12 @@ class AuthenticationController extends ActionController
if (! $auth->isAuthenticated()) {
$this->redirectToLogin();
}
$isRemoteUser = $auth->getUser()->isRemoteUser();
// Get info whether the user is externally authenticated before removing authorization which destroys the
// session and the user object
$isExternalUser = $auth->getUser()->isExternalUser();
$auth->removeAuthorization();
if ($isRemoteUser === true) {
$this->_response->setHttpResponseCode(401);
if ($isExternalUser) {
$this->getResponse()->setHttpResponseCode(401);
} else {
$this->redirectToLogin();
}

View File

@ -3,16 +3,24 @@
namespace Icinga\Forms\Authentication;
use Icinga\Authentication\Auth;
use Icinga\Authentication\User\ExternalBackend;
use Icinga\User;
use Icinga\Web\Form;
use Icinga\Web\Url;
/**
* Class LoginForm
* Form for user authentication
*/
class LoginForm extends Form
{
/**
* Initialize this login form
* Redirect URL
*/
const REDIRECT_URL = 'dashboard';
/**
* {@inheritdoc}
*/
public function init()
{
@ -22,7 +30,7 @@ class LoginForm extends Form
}
/**
* @see Form::createElements()
* {@inheritdoc}
*/
public function createElements(array $formData)
{
@ -54,4 +62,83 @@ class LoginForm extends Form
)
);
}
/**
* {@inheritdoc}
*/
public function getRedirectUrl()
{
$redirect = null;
if ($this->created) {
$redirect = $this->getElement('redirect')->getValue();
}
if (empty($redirect)) {
$redirect = static::REDIRECT_URL;
}
return Url::fromPath($redirect);
}
/**
* {@inheritdoc}
*/
public function onSuccess()
{
$auth = Auth::getInstance();
$authChain = $auth->getAuthChain();
$authChain->setSkipExternalBackends(true);
$user = new User($this->getElement('username')->getValue());
$password = $this->getElement('password')->getValue();
$authenticated = $authChain->authenticate($user, $password);
if ($authenticated) {
$auth->setAuthenticated($user);
$this->getResponse()->setRerenderLayout(true);
return true;
}
switch ($authChain->getError()) {
case $authChain::EEMPTY:
$this->addError($this->translate(
'No authentication methods available.'
. ' Did you create authentication.ini when setting up Icinga Web 2?'
));
break;
case $authChain::EFAIL:
$this->addError($this->translate(
'All configured authentication methods failed.'
. ' Please check the system log or Icinga Web 2 log for more information.'
));
break;
/** @noinspection PhpMissingBreakStatementInspection */
case $authChain::ENOTALL:
$this->addError($this->translate(
'Please note that not all authentication methods were available.'
. ' Check the system log or Icinga Web 2 log for more information.'
));
// Move to default
default:
$this->getElement('password')->addError($this->translate('Incorrect username or password'));
break;
}
return false;
}
/**
* {@inheritdoc}
*/
public function onRequest()
{
$auth = Auth::getInstance();
$onlyExternal = true;
// TODO(el): This may be set on the auth chain once iterated. See Auth::authExternal().
foreach ($auth->getAuthChain() as $backend) {
if (! $backend instanceof ExternalBackend) {
$onlyExternal = false;
}
}
if ($onlyExternal) {
$this->addError($this->translate(
'You\'re currently not authenticated using any of the web server\'s authentication mechanisms.'
. ' Make sure you\'ll configure such, otherwise you\'ll not be able to login.'
));
}
}
}

View File

@ -6,7 +6,7 @@ namespace Icinga\Forms;
use Exception;
use DateTimeZone;
use Icinga\Application\Logger;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\User\Preferences;
use Icinga\User\Preferences\PreferencesStore;
use Icinga\Util\TimezoneDetect;
@ -123,7 +123,7 @@ class PreferenceForm extends Form
*/
public function onRequest()
{
$auth = Manager::getInstance();
$auth = Auth::getInstance();
$values = $auth->getUser()->getPreferences()->get('icingaweb');
if (! isset($values['language'])) {

View File

@ -2,7 +2,7 @@
use Icinga\Web\Url;
use Icinga\Web\Notification;
use Icinga\Authentication\Manager as Auth;
use Icinga\Authentication\Auth;
if (Auth::getInstance()->isAuthenticated()):
@ -51,7 +51,7 @@ if (Auth::getInstance()->isAuthenticated()):
<ul id="notifications"><?php
$notifications = Notification::getInstance();
if ($notifications->hasMessages()) {
foreach ($notifications->getMessages() as $m) {
foreach ($notifications->popMessages() as $m) {
echo '<li class="' . $m->type . '">' . $this->escape($m->message) . '</li>';
}
}

View File

@ -17,11 +17,6 @@
'<a href="' . $this->href('setup') . '" title="' . $this->translate('Icinga Web 2 Setup-Wizard') . '">',
'</a>'
); ?></p>
<?php elseif ($requiresExternalAuth): ?>
<p class="info-box"><?= $this->icon('info'); ?><?= $this->translate(
'You\'re currently not authenticated using any of the web server\'s authentication mechanisms.'
. ' Make sure you\'ll configure such, otherwise you\'ll not be able to login.'
); ?></p>
<?php endif ?>
<?= $this->form ?>
<div class="footer">Icinga Web 2 &copy; 2013-2015<br><a href="https://www.icinga.org"><?= $this->translate('The Icinga Project'); ?></a></div>

View File

@ -12,7 +12,7 @@ use Zend_Layout;
use Zend_Paginator;
use Zend_View_Helper_PaginationControl;
use Icinga\Application\Logger;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\User;
use Icinga\Util\TimezoneDetect;
use Icinga\Util\Translator;
@ -55,6 +55,13 @@ class Web extends ApplicationBootstrap
*/
private $request;
/**
* Response
*
* @var Response
*/
protected $response;
/**
* Session object
*
@ -91,11 +98,12 @@ class Web extends ApplicationBootstrap
->setupResourceFactory()
->setupSession()
->setupNotifications()
->setupRequest()
->setupResponse()
->setupUser()
->setupTimezone()
->setupLogger()
->setupInternationalization()
->setupRequest()
->setupZendMvc()
->setupFormNamespace()
->setupModuleManager()
@ -136,6 +144,26 @@ class Web extends ApplicationBootstrap
return $this->frontController;
}
/**
* Get the request
*
* @return Request
*/
public function getRequest()
{
return $this->request;
}
/**
* Get the response
*
* @return Response
*/
public function getResponse()
{
return $this->response;
}
/**
* Getter for view
*
@ -151,7 +179,7 @@ class Web extends ApplicationBootstrap
*/
public function dispatch()
{
$this->frontController->dispatch($this->request, new Response());
$this->frontController->dispatch($this->getRequest(), $this->getResponse());
}
/**
@ -179,9 +207,11 @@ class Web extends ApplicationBootstrap
*/
private function setupUser()
{
$auth = Manager::getInstance();
$auth = Auth::getInstance();
if ($auth->isAuthenticated()) {
$this->user = $auth->getUser();
$user = $auth->getUser();
$this->request->setUser($user);
$this->user = $user;
}
return $this;
}
@ -209,16 +239,24 @@ class Web extends ApplicationBootstrap
}
/**
* Inject dependencies into request
* Set the request
*
* @return $this
*/
private function setupRequest()
{
$this->request = new Request();
if ($this->user instanceof User) {
$this->request->setUser($this->user);
}
return $this;
}
/**
* Set the response
*
* @return $this
*/
protected function setupResponse()
{
$this->response = new Response();
return $this;
}
@ -282,7 +320,7 @@ class Web extends ApplicationBootstrap
*/
protected function detectTimezone()
{
$auth = Manager::getInstance();
$auth = Auth::getInstance();
if (! $auth->isAuthenticated()
|| ($timezone = $auth->getUser()->getPreferences()->getValue('icingaweb', 'timezone')) === null
) {
@ -303,7 +341,7 @@ class Web extends ApplicationBootstrap
*/
protected function detectLocale()
{
$auth = Manager::getInstance();
$auth = Auth::getInstance();
if ($auth->isAuthenticated()
&& ($locale = $auth->getUser()->getPreferences()->getValue('icingaweb', 'language')) !== null
) {

View File

@ -4,18 +4,20 @@
namespace Icinga\Authentication;
use Exception;
use Icinga\Authentication\UserGroup\UserGroupBackend;
use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Authentication\User\ExternalBackend;
use Icinga\Authentication\UserGroup\UserGroupBackend;
use Icinga\Data\ConfigObject;
use Icinga\Exception\IcingaException;
use Icinga\Exception\NotReadableError;
use Icinga\Application\Logger;
use Icinga\User;
use Icinga\User\Preferences;
use Icinga\User\Preferences\PreferencesStore;
use Icinga\Web\Session;
class Manager
class Auth
{
/**
* Singleton instance
@ -24,6 +26,20 @@ class Manager
*/
private static $instance;
/**
* Request
*
* @var \Icinga\Web\Request
*/
protected $request;
/**
* Response
*
* @var \Icinga\Web\Response
*/
protected $response;
/**
* Authenticated user
*
@ -32,6 +48,9 @@ class Manager
private $user;
/**
* @see getInstance()
*/
private function __construct()
{
}
@ -44,11 +63,39 @@ class Manager
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new static();
self::$instance = new self();
}
return self::$instance;
}
/**
* Get the auth chain
*
* @return AuthChain
*/
public function getAuthChain()
{
return new AuthChain();
}
/**
* Whether the user is authenticated
*
* @param bool $ignoreSession True to prevent session authentication
*
* @return bool
*/
public function isAuthenticated($ignoreSession = false)
{
if ($this->user === null && ! $ignoreSession) {
$this->authenticateFromSession();
}
if ($this->user === null && ! $this->authExternal()) {
return $this->authHttp();
}
return true;
}
public function setAuthenticated(User $user, $persist = true)
{
$username = $user->getUsername();
@ -121,58 +168,40 @@ class Manager
}
/**
* Writes the current user to the session
* Getter for groups belonged to authenticated user
*
* @return array
* @see User::getGroups
*/
public function persistCurrentUser()
public function getGroups()
{
Session::getSession()->set('user', $this->user)->refreshId();
return $this->user->getGroups();
}
/**
* Try to authenticate the user with the current session
* Get the request
*
* Authentication for externally-authenticated users will be revoked if the username changed or external
* authentication is no longer in effect
* @return \Icinga\Web\Request
*/
public function authenticateFromSession()
public function getRequest()
{
$this->user = Session::getSession()->get('user');
if ($this->user !== null && $this->user->isRemoteUser() === true) {
list($originUsername, $field) = $this->user->getRemoteUserInformation();
if (! array_key_exists($field, $_SERVER) || $_SERVER[$field] !== $originUsername) {
$this->removeAuthorization();
}
if ($this->request === null) {
$this->request = Icinga::app()->getRequest();
}
return $this->request;
}
/**
* Whether the user is authenticated
* Get the response
*
* @param bool $ignoreSession True to prevent session authentication
*
* @return bool
* @return \Icinga\Web\Response
*/
public function isAuthenticated($ignoreSession = false)
public function getResponse()
{
if ($this->user === null && ! $ignoreSession) {
$this->authenticateFromSession();
if ($this->response === null) {
$this->response = Icinga::app()->getResponse();
}
return is_object($this->user);
}
/**
* Whether an authenticated user has a given permission
*
* @param string $permission Permission name
*
* @return bool True if the user owns the given permission, false if not or if not authenticated
*/
public function hasPermission($permission)
{
if (! $this->isAuthenticated()) {
return false;
}
return $this->user->can($permission);
return $this->response;
}
/**
@ -192,15 +221,6 @@ class Manager
return $this->user->getRestrictions($restriction);
}
/**
* Purges the current authorization information and session
*/
public function removeAuthorization()
{
$this->user = null;
Session::getSession()->purge();
}
/**
* Returns the current user or null if no user is authenticated
*
@ -212,13 +232,124 @@ class Manager
}
/**
* Getter for groups belonged to authenticated user
* Try to authenticate the user with the current session
*
* @return array
* @see User::getGroups
* Authentication for externally-authenticated users will be revoked if the username changed or external
* authentication is no longer in effect
*/
public function getGroups()
public function authenticateFromSession()
{
return $this->user->getGroups();
$this->user = Session::getSession()->get('user');
if ($this->user !== null && $this->user->isExternalUser() === true) {
list($originUsername, $field) = $this->user->getExternalUserInformation();
if (! array_key_exists($field, $_SERVER) || $_SERVER[$field] !== $originUsername) {
$this->removeAuthorization();
}
}
}
/**
* Attempt to authenticate a user from external user backends
*
* @return bool
*/
protected function authExternal()
{
$user = new User('');
foreach ($this->getAuthChain() as $userBackend) {
if ($userBackend instanceof ExternalBackend) {
if ($userBackend->authenticate($user)) {
$this->setAuthenticated($user);
return true;
}
}
}
return false;
}
/**
* Attempt to authenticate a user using HTTP authentication
*
* Supports only the Basic HTTP authentication scheme. XHR will be ignored.
*
* @return bool
*/
protected function authHttp()
{
if ($this->getRequest()->isXmlHttpRequest()) {
return false;
}
if (($header = $this->getRequest()->getHeader('Authorization')) === false) {
return false;
}
if (empty($header)) {
$this->challengeHttp();
}
list($scheme) = explode(' ', $header, 2);
if ($scheme !== 'Basic') {
$this->challengeHttp();
}
$authorization = substr($header, strlen('Basic '));
$credentials = base64_decode($authorization);
$credentials = array_filter(explode(':', $credentials, 2));
if (count($credentials) !== 2) {
// Deny empty username and/or password
$this->challengeHttp();
}
$user = new User($credentials[0]);
$password = $credentials[1];
if ($this->getAuthChain()->setSkipExternalBackends(true)->authenticate($user, $password)) {
$this->setAuthenticated($user, false);
$user->setIsHttpUser(true);
return true;
} else {
$this->challengeHttp();
}
}
/**
* Challenge client immediately for HTTP authentication
*
* Sends the response w/ the 401 Unauthorized status code and WWW-Authenticate header.
*/
protected function challengeHttp()
{
$response = $this->getResponse();
$response->setHttpResponseCode(401);
$response->setHeader('WWW-Authenticate', 'Basic realm="Icinga Web 2"');
$response->sendHeaders();
exit();
}
/**
* Whether an authenticated user has a given permission
*
* @param string $permission Permission name
*
* @return bool True if the user owns the given permission, false if not or if not authenticated
*/
public function hasPermission($permission)
{
if (! $this->isAuthenticated()) {
return false;
}
return $this->user->can($permission);
}
/**
* Writes the current user to the session
*/
public function persistCurrentUser()
{
Session::getSession()->set('user', $this->user)->refreshId();
}
/**
* Purges the current authorization information and session
*/
public function removeAuthorization()
{
$this->user = null;
Session::getSession()->purge();
}
}

View File

@ -4,46 +4,180 @@
namespace Icinga\Authentication;
use Iterator;
use Icinga\Data\ConfigObject;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\UserBackendInterface;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Icinga\Authentication\User\ExternalBackend;
use Icinga\Authentication\User\UserBackend;
use Icinga\Authentication\User\UserBackendInterface;
use Icinga\Data\ConfigObject;
use Icinga\Exception\AuthenticationException;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\NotReadableError;
use Icinga\User;
/**
* Iterate user backends created from config
*/
class AuthChain implements Iterator
class AuthChain implements Authenticatable, Iterator
{
/**
* Authentication config file
*
* @var string
*/
const AUTHENTICATION_CONFIG = 'authentication';
/**
* Error code if the authentication configuration was not readable
*
* @var int
*/
const EPERM = 1;
/**
* Error code if the authentication configuration is empty
*/
const EEMPTY = 2;
/**
* Error code if all authentication methods failed
*
* @var int
*/
const EFAIL = 3;
/**
* Error code if not all authentication methods were available
*
* @var int
*/
const ENOTALL = 4;
/**
* User backends configuration
*
* @var Config
*/
private $config;
protected $config;
/**
* The consecutive user backend while looping
*
* @var UserBackendInterface
*/
private $currentBackend;
protected $currentBackend;
/**
* Last error code
*
* @var int|null
*/
protected $error;
/**
* Whether external user backends should be skipped on iteration
*
* @var bool
*/
protected $skipExternalBackends = false;
/**
* Create a new authentication chain from config
*
* @param Config $config User backends configuration
*/
public function __construct(Config $config)
public function __construct(Config $config = null)
{
$this->config = $config;
if ($config === null) {
try {
$this->config = Config::app(static::AUTHENTICATION_CONFIG);
} catch (NotReadableError $e) {
$this->config = new Config();
$this->error = static::EPERM;
}
} else {
$this->config = $config;
}
}
/**
* {@inheritdoc}
*/
public function authenticate(User $user, $password)
{
$this->error = null;
$backendsTried = 0;
$backendsWithError = 0;
foreach ($this as $backend) {
++$backendsTried;
try {
$authenticated = $backend->authenticate($user, $password);
} catch (AuthenticationException $e) {
Logger::error($e);
++$backendsWithError;
continue;
}
if ($authenticated) {
return true;
}
}
if ($backendsTried === 0) {
$this->error = static::EEMPTY;
} elseif ($backendsTried === $backendsWithError) {
$this->error = static::EFAIL;
} elseif ($backendsWithError) {
$this->error = static::ENOTALL;
}
return false;
}
/**
* Get the last error code
*
* @return int|null
*/
public function getError()
{
return $this->error;
}
/**
* Whether authentication had errors
*
* @return bool
*/
public function hasError()
{
return $this->error !== null;
}
/**
* Get whether to skip external user backends on iteration
*
* @return bool
*/
public function getSkipExternalBackends()
{
return $this->skipExternalBackends;
}
/**
* Set whether to skip external user backends on iteration
*
* @param bool $skipExternalBackends
*
* @return $this
*/
public function setSkipExternalBackends($skipExternalBackends = true)
{
$this->skipExternalBackends = (bool) $skipExternalBackends;
return $this;
}
/**
* Rewind the chain
*
* @return ConfigObject
* @return ConfigObject
*/
public function rewind()
{
@ -52,7 +186,7 @@ class AuthChain implements Iterator
}
/**
* Return the current user backend
* Get the current user backend
*
* @return UserBackendInterface
*/
@ -62,9 +196,9 @@ class AuthChain implements Iterator
}
/**
* Return the key of the current user backend config
* Get the key of the current user backend config
*
* @return string
* @return string
*/
public function key()
{
@ -74,7 +208,7 @@ class AuthChain implements Iterator
/**
* Move forward to the next user backend config
*
* @return ConfigObject
* @return ConfigObject
*/
public function next()
{
@ -82,19 +216,20 @@ class AuthChain implements Iterator
}
/**
* Check if the current user backend is valid, i.e. it's enabled and the config is valid
* Check whether the current user backend is valid, i.e. it's enabled, not an external user backend and whether its
* config is valid
*
* @return bool
* @return bool
*/
public function valid()
{
if (!$this->config->valid()) {
if (! $this->config->valid()) {
// Stop when there are no more backends to check
return false;
}
$backendConfig = $this->config->current();
if ((bool) $backendConfig->get('disabled', false) === true) {
if ((bool) $backendConfig->get('disabled', false)) {
$this->next();
return $this->valid();
}
@ -105,15 +240,20 @@ class AuthChain implements Iterator
} catch (ConfigurationError $e) {
Logger::error(
new ConfigurationError(
'Cannot create authentication backend "%s". An exception was thrown:',
$name,
$e
'Can\'t create authentication backend "%s". An exception was thrown:', $name, $e
)
);
$this->next();
return $this->valid();
}
if ($this->getSkipExternalBackends()
&& $backend instanceof ExternalBackend
) {
$this->next();
return $this->valid();
}
$this->currentBackend = $backend;
return true;
}

View File

@ -0,0 +1,21 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Authentication;
use Icinga\User;
interface Authenticatable
{
/**
* Authenticate a user
*
* @param User $user
* @param string $password
*
* @return bool
*
* @throws \Icinga\Exception\AuthenticationException If authentication errors
*/
public function authenticate(User $user, $password);
}

View File

@ -36,11 +36,7 @@ class ExternalBackend implements UserBackendInterface
}
/**
* Set this backend's name
*
* @param string $name
*
* @return $this
* {@inheritdoc}
*/
public function setName($name)
{
@ -49,30 +45,22 @@ class ExternalBackend implements UserBackendInterface
}
/**
* Return this backend's name
*
* @return string
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* Authenticate the given user
*
* @param User $user
* @param string $password
*
* @return bool True on success, false on failure
*
* @throws AuthenticationException In case authentication is not possible due to an error
* {@inheritdoc}
*/
public function authenticate(User $user, $password = null)
{
if (isset($_SERVER['REMOTE_USER'])) {
$username = $_SERVER['REMOTE_USER'];
$user->setRemoteUserInformation($username, 'REMOTE_USER');
$user->setExternalUserInformation($username, 'REMOTE_USER');
if ($this->stripUsernameRegexp) {
$stripped = preg_replace($this->stripUsernameRegexp, '', $username);

View File

@ -3,13 +3,13 @@
namespace Icinga\Authentication\User;
use Icinga\Exception\AuthenticationException;
use Icinga\Authentication\Authenticatable;
use Icinga\User;
/**
* Interface for user backends
*/
interface UserBackendInterface
interface UserBackendInterface extends Authenticatable
{
/**
* Set this backend's name
@ -26,16 +26,4 @@ interface UserBackendInterface
* @return string
*/
public function getName();
/**
* Authenticate the given user
*
* @param User $user
* @param string $password
*
* @return bool True on success, false on failure
*
* @throws AuthenticationException In case authentication is not possible due to an error
*/
public function authenticate(User $user, $password);
}

View File

@ -111,7 +111,7 @@ class PivotTable
*/
protected function getPaginationParameter($axis, $param, $default = null)
{
$request = Icinga::app()->getFrontController()->getRequest();
$request = Icinga::app()->getRequest();
$value = $request->getParam($param, '');
if (strpos($value, ',') > 0) {

View File

@ -435,7 +435,7 @@ class SimpleQuery implements QueryInterface, Queryable, Iterator
if ($itemsPerPage === null || $pageNumber === null) {
// Detect parameters from request
$request = Icinga::app()->getFrontController()->getRequest();
$request = Icinga::app()->getRequest();
if ($itemsPerPage === null) {
$itemsPerPage = $request->getParam('limit', 25);
}

View File

@ -180,7 +180,7 @@ namespace Icinga\Test {
*/
public function getRequestMock()
{
return Icinga::app()->getFrontController()->getRequest();
return Icinga::app()->getRequest();
}
/**

View File

@ -57,7 +57,7 @@ class User
protected $additionalInformation = array();
/**
* Information if the user is external authenticated
* Information if the user is externally authenticated
*
* Keys:
*
@ -66,7 +66,7 @@ class User
*
* @var array
*/
protected $remoteUserInformation = array();
protected $externalUserInformation = array();
/**
* Set of permissions
@ -96,6 +96,13 @@ class User
*/
protected $preferences;
/**
* Whether the user is authenticated using a HTTP authentication mechanism
*
* @var bool
*/
protected $isHttpUser = false;
/**
* Creates a user object given the provided information
*
@ -380,34 +387,57 @@ class User
}
/**
* Set additional remote user information
* Set additional external user information
*
* @param stirng $username
* @param string $username
* @param string $field
*/
public function setRemoteUserInformation($username, $field)
public function setExternalUserInformation($username, $field)
{
$this->remoteUserInformation = array($username, $field);
$this->externalUserInformation = array($username, $field);
}
/**
* Get additional remote user information
* Get additional external user information
*
* @return array
*/
public function getRemoteUserInformation()
public function getExternalUserInformation()
{
return $this->remoteUserInformation;
return $this->externalUserInformation;
}
/**
* Return true if user has remote user information set
* Return true if user has external user information set
*
* @return bool
*/
public function isRemoteUser()
public function isExternalUser()
{
return ! empty($this->remoteUserInformation);
return ! empty($this->externalUserInformation);
}
/**
* Get whether the user is authenticated using a HTTP authentication mechanism
*
* @return bool
*/
public function getIsHttpUser()
{
return $this->isHttpUser;
}
/**
* Set whether the user is authenticated using a HTTP authentication mechanism
*
* @param bool $isHttpUser
*
* @return $this
*/
public function setIsHttpUser($isHttpUser = true)
{
$this->isHttpUser = (bool) $isHttpUser;
return $this;
}
/**

View File

@ -6,7 +6,7 @@ namespace Icinga\Web\Controller;
use Exception;
use Icinga\Application\Benchmark;
use Icinga\Application\Config;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\Exception\Http\HttpMethodNotAllowedException;
use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
@ -52,7 +52,7 @@ class ActionController extends Zend_Controller_Action
/**
* Authentication manager
*
* @var Manager|null
* @var Auth|null
*/
private $auth;
@ -124,12 +124,12 @@ class ActionController extends Zend_Controller_Action
/**
* Get the authentication manager
*
* @return Manager
* @return Auth
*/
public function Auth()
{
if ($this->auth === null) {
$this->auth = Manager::getInstance();
$this->auth = Auth::getInstance();
}
return $this->auth;
}
@ -455,7 +455,7 @@ class ActionController extends Zend_Controller_Action
$notifications = Notification::getInstance();
if ($notifications->hasMessages()) {
$notificationList = array();
foreach ($notifications->getMessages() as $m) {
foreach ($notifications->popMessages() as $m) {
$notificationList[] = rawurlencode($m->type . ' ' . $m->message);
}
$resp->setHeader('X-Icinga-Notification', implode('&', $notificationList), true);

View File

@ -8,7 +8,7 @@ use Zend_Form;
use Zend_Form_Element;
use Zend_View_Interface;
use Icinga\Application\Icinga;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\Exception\ProgrammingError;
use Icinga\Security\SecurityException;
use Icinga\Util\Translator;
@ -179,7 +179,7 @@ class Form extends Zend_Form
/**
* Authentication manager
*
* @var Manager|null
* @var Auth|null
*/
private $auth;
@ -948,10 +948,17 @@ class Form extends Zend_Form
*/
public function addCsrfCounterMeasure()
{
if (! $this->tokenDisabled && $this->getElement($this->tokenElementName) === null) {
$this->addElement(new CsrfCounterMeasure($this->tokenElementName));
if (! $this->tokenDisabled) {
$request = $this->getRequest();
if (! $request->isXmlHttpRequest()
&& $request->getIsApiRequest()
) {
return $this;
}
if ($this->getElement($this->tokenElementName) === null) {
$this->addElement(new CsrfCounterMeasure($this->tokenElementName));
}
}
return $this;
}
@ -1245,7 +1252,7 @@ class Form extends Zend_Form
public function getRequest()
{
if ($this->request === null) {
$this->request = Icinga::app()->getFrontController()->getRequest();
$this->request = Icinga::app()->getRequest();
}
return $this->request;
@ -1344,12 +1351,12 @@ class Form extends Zend_Form
/**
* Get the authentication manager
*
* @return Manager
* @return Auth
*/
public function Auth()
{
if ($this->auth === null) {
$this->auth = Manager::getInstance();
$this->auth = Auth::getInstance();
}
return $this->auth;
}

View File

@ -75,6 +75,6 @@ class Button extends FormElement
*/
protected function getRequest()
{
return Icinga::app()->getFrontController()->getRequest();
return Icinga::app()->getRequest();
}
}

View File

@ -7,7 +7,7 @@ use RecursiveIterator;
use Icinga\Application\Config;
use Icinga\Application\Icinga;
use Icinga\Application\Logger;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\Data\ConfigObject;
use Icinga\Exception\ConfigurationError;
use Icinga\Exception\ProgrammingError;
@ -208,7 +208,7 @@ class Menu implements RecursiveIterator
{
$menu = new static('menu');
$menu->addMainMenuItems();
$auth = Manager::getInstance();
$auth = Auth::getInstance();
$manager = Icinga::app()->getModuleManager();
foreach ($manager->getLoadedModules() as $module) {
if ($auth->hasPermission($manager::MODULE_PERMISSION_NS . $module->getName())) {
@ -223,7 +223,7 @@ class Menu implements RecursiveIterator
*/
protected function addMainMenuItems()
{
$auth = Manager::getInstance();
$auth = Auth::getInstance();
if ($auth->isAuthenticated()) {

View File

@ -3,7 +3,7 @@
namespace Icinga\Web\Menu;
use RecursiveFilterIterator;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\Web\Menu;
class PermittedMenuItemFilter extends RecursiveFilterIterator
@ -18,7 +18,7 @@ class PermittedMenuItemFilter extends RecursiveFilterIterator
$item = $this->current();
/** @var Menu $item */
if (($permission = $item->getPermission()) !== null) {
$auth = Manager::getInstance();
$auth = Auth::getInstance();
if (! $auth->isAuthenticated()) {
// Don't accept menu item because user is not authenticated and the menu item requires a permission
return false;

View File

@ -17,86 +17,72 @@ use Icinga\Web\Session;
*/
class Notification
{
/**
* Notification type info
*
* @var string
*/
const INFO = 'info';
/**
* Notification type error
*
* @var string
*/
const ERROR = 'error';
/**
* Notification type success
*
* @var string
*/
const SUCCESS = 'success';
/**
* Notification type warning
*
* @var string
*/
const WARNING = 'warning';
/**
* Name of the session key for notification messages
*
* @var string
*/
const SESSION_KEY = 'session';
/**
* Singleton instance
*
* @var self
*/
protected static $instance;
/**
* Whether the platform is CLI
*
* @var bool
*/
protected $isCli = false;
protected $session;
/**
* Notification messages
*
* @var array
*/
protected $messages = array();
public static function info($msg)
{
self::getInstance()->addMessage($msg, 'info');
}
public static function success($msg)
{
self::getInstance()->addMessage($msg, 'success');
}
public static function warning($msg)
{
self::getInstance()->addMessage($msg, 'warning');
}
public static function error($msg)
{
self::getInstance()->addMessage($msg, 'error');
}
protected function addMessage($message, $type = 'info')
{
if (! in_array(
$type,
array(
'info',
'error',
'warning',
'success'
)
)) {
throw new ProgrammingError(
'"%s" is not a valid notification type',
$type
);
}
if ($this->isCli) {
$msg = sprintf('[%s] %s', $type, $message);
switch ($type) {
case 'info':
case 'success':
Logger::info($msg);
break;
case 'warning':
Logger::warn($msg);
break;
case 'error':
Logger::error($msg);
break;
}
return;
}
$this->messages[] = (object) array(
'type' => $type,
'message' => $message,
);
}
public function hasMessages()
{
return false === empty($this->messages);
}
public function getMessages()
{
$messages = $this->messages;
$this->messages = array();
return $messages;
}
/**
* Session
*
* @var Session
*/
protected $session;
/**
* Create the notification instance
*/
final private function __construct()
{
if (Platform::isCli()) {
@ -105,33 +91,130 @@ class Notification
}
$this->session = Session::getSession();
$stored = $this->session->get('messages');
if (is_array($stored)) {
$this->messages = $stored;
$this->session->set('messages', array());
$messages = $this->session->get(self::SESSION_KEY);
if (is_array($messages)) {
$this->messages = $messages;
$this->session->delete(self::SESSION_KEY);
$this->session->write();
}
}
/**
* Get the Notification instance
*
* @return Notification
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new Notification();
self::$instance = new self();
}
return self::$instance;
}
/**
* Add info notification
*
* @param string $msg
*/
public static function info($msg)
{
self::getInstance()->addMessage($msg, self::INFO);
}
/**
* Add error notification
*
* @param string $msg
*/
public static function error($msg)
{
self::getInstance()->addMessage($msg, self::ERROR);
}
/**
* Add success notification
*
* @param string $msg
*/
public static function success($msg)
{
self::getInstance()->addMessage($msg, self::SUCCESS);
}
/**
* Add warning notification
*
* @param string $msg
*/
public static function warning($msg)
{
self::getInstance()->addMessage($msg, self::WARNING);
}
/**
* Add a notification message
*
* @param string $message
* @param string $type
*/
protected function addMessage($message, $type = self::INFO)
{
if ($this->isCli) {
$msg = sprintf('[%s] %s', $type, $message);
switch ($type) {
case self::INFO:
case self::SUCCESS:
Logger::info($msg);
break;
case self::ERROR:
Logger::error($msg);
break;
case self::WARNING:
Logger::warning($msg);
break;
}
} else {
$this->messages[] = (object) array(
'type' => $type,
'message' => $message,
);
}
}
/**
* Pop the notification messages
*
* @return array
*/
public function popMessages()
{
$messages = $this->messages;
$this->messages = array();
return $messages;
}
/**
* Get whether notification messages have been added
*
* @return bool
*/
public function hasMessages()
{
return ! empty($this->messages);
}
/**
* Destroy the notification instance
*/
final public function __destruct()
{
if ($this->isCli) {
return;
}
if ($this->session->get('messages') !== $this->messages) {
$this->session->set('messages', $this->messages);
if ($this->hasMessages() && $this->session->get('messages') !== $this->messages) {
$this->session->set(self::SESSION_KEY, $this->messages);
$this->session->write();
}
$this->session->write();
unset($this->session);
}
}

View File

@ -7,24 +7,46 @@ use Zend_Controller_Request_Http;
use Icinga\User;
/**
* Request to handle special attributes
* A request
*/
class Request extends Zend_Controller_Request_Http
{
/**
* User object
* User if authenticated
*
* @var User
* @var User|null
*/
private $user;
protected $user;
/**
* Unique identifier
*
* @var string
*/
private $uniqueId;
protected $uniqueId;
private $url;
/**
* Request URL
*
* @var Url
*/
protected $url;
/**
* Get whether the request seems to be an API request
*
* @return bool
*/
public function getIsApiRequest()
{
return $this->getHeader('Accept') === 'application/json';
}
/**
* Get the request URL
*
* @return Url
*/
public function getUrl()
{
if ($this->url === null) {
@ -34,31 +56,38 @@ class Request extends Zend_Controller_Request_Http
}
/**
* Setter for user
* Get the user if authenticated
*
* @param User $user
*/
public function setUser(User $user)
{
$this->user = $user;
}
/**
* Getter for user
*
* @return User
* @return User|null
*/
public function getUser()
{
return $this->user;
}
/**
* Set the authenticated user
*
* @param User $user
*
* @return $this
*/
public function setUser(User $user)
{
$this->user = $user;
return $this;
}
/**
* Makes an ID unique to this request, to prevent id collisions in different containers
*
* Call this whenever an ID might show up multiple times in different containers. This function is useful
* for ensuring unique ids on sites, even if we combine the HTML of different requests into one site,
* while still being able to reference elements uniquely in the same request.
*
* @param string $id
*
* @return string The id suffixed w/ an identifier unique to this request
*/
public function protectId($id)
{

View File

@ -8,18 +8,125 @@ use Icinga\Application\Icinga;
class Response extends Zend_Controller_Response_Http
{
/**
* Redirect URL
*
* @var Url|null
*/
protected $redirectUrl;
/**
* Request
*
* @var Request
*/
protected $request;
/**
* Whether to send the rerender layout header on XHR
*
* @var bool
*/
protected $rerenderLayout = false;
/**
* Get the redirect URL
*
* @return Url|null
*/
protected function getRedirectUrl()
{
return $this->redirectUrl;
}
/**
* Set the redirect URL
*
* Unlike {@link setRedirect()} this method only sets a redirect URL on the response for later usage.
* {@link prepare()} will take care of the correct redirect handling and HTTP headers on XHR and "normal" browser
* requests.
*
* @param string|Url $redirectUrl
*
* @return $this
*/
protected function setRedirectUrl($redirectUrl)
{
if (! $redirectUrl instanceof Url) {
$redirectUrl = Url::fromPath((string) $redirectUrl);
}
$redirectUrl->getParams()->setSeparator('&');
$this->redirectUrl = $redirectUrl;
return $this;
}
/**
* Get the request
*
* @return Request
*/
public function getRequest()
{
if ($this->request === null) {
$this->request = Icinga::app()->getRequest();
}
return $this->request;
}
/**
* Get whether to send the rerender layout header on XHR
*
* @return bool
*/
public function getRerenderLayout()
{
return $this->rerenderLayout;
}
/**
* Get whether to send the rerender layout header on XHR
*
* @param bool $rerenderLayout
*
* @return $this
*/
public function setRerenderLayout($rerenderLayout = true)
{
$this->rerenderLayout = (bool) $rerenderLayout;
return $this;
}
/**
* Prepare the request before sending
*/
protected function prepare()
{
$redirectUrl = $this->getRedirectUrl();
if ($this->getRequest()->isXmlHttpRequest()) {
if ($redirectUrl !== null) {
$this->setHeader('X-Icinga-Redirect', rawurlencode($redirectUrl->getAbsoluteUrl()), true);
if ($this->getRerenderLayout()) {
$this->setHeader('X-Icinga-Rerender-Layout', 'yes', true);
}
}
if ($this->getRerenderLayout()) {
$this->setHeader('X-Icinga-Container', 'layout', true);
}
} else {
if ($redirectUrl !== null) {
$this->setRedirect($redirectUrl->getAbsoluteUrl());
}
}
}
/**
* Redirect to the given URL and exit immediately
*
* @param string|Url $url
*/
public function redirectAndExit($url)
{
if (! $url instanceof Url) {
$url = Url::fromPath($url);
}
$url->getParams()->setSeparator('&');
if (Icinga::app()->getFrontController()->getRequest()->isXmlHttpRequest()) {
$this->setHeader('X-Icinga-Redirect', rawurlencode($url->getAbsoluteUrl()));
} else {
$this->setRedirect($url->getAbsoluteUrl());
}
$this->setRedirectUrl($url);
$session = Session::getSession();
if ($session->hasChanged()) {
@ -29,4 +136,13 @@ class Response extends Zend_Controller_Response_Http
$this->sendHeaders();
exit;
}
/**
* {@inheritdoc}
*/
public function sendHeaders()
{
$this->prepare();
return parent::sendHeaders();
}
}

View File

@ -102,7 +102,7 @@ class Url
if ($app->isCli()) {
return new FakeRequest();
} else {
return $app->getFrontController()->getRequest();
return $app->getRequest();
}
}

View File

@ -5,7 +5,7 @@ namespace Icinga\Web;
use Closure;
use Zend_View_Abstract;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\Exception\ProgrammingError;
use Icinga\Util\Translator;
@ -39,7 +39,7 @@ class View extends Zend_View_Abstract
/**
* Authentication manager
*
* @var \Icinga\Authentication\Manager|null
* @var Auth|null
*/
private $auth;
@ -164,12 +164,12 @@ class View extends Zend_View_Abstract
/**
* Get the authentication manager
*
* @return Manager
* @return Auth
*/
public function Auth()
{
if ($this->auth === null) {
$this->auth = Manager::getInstance();
$this->auth = Auth::getInstance();
}
return $this->auth;
}

View File

@ -3,11 +3,11 @@
namespace Icinga\Web\View;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\Web\Widget;
$this->addHelperFunction('auth', function () {
return Manager::getInstance();
return Auth::getInstance();
});
$this->addHelperFunction('widget', function ($name, $options = null) {

View File

@ -116,7 +116,7 @@ class SortBox extends AbstractWidget
{
if ($this->query !== null) {
if ($request === null) {
$request = Icinga::app()->getFrontController()->getRequest();
$request = Icinga::app()->getRequest();
}
if (($sort = $request->getParam('sort'))) {

View File

@ -309,7 +309,7 @@ EOT;
private function renderRefreshTab()
{
$url = Icinga::app()->getFrontController()->getRequest()->getUrl();
$url = Icinga::app()->getRequest()->getUrl();
$tab = $this->get($this->getActiveName());
if ($tab !== null) {

View File

@ -3,7 +3,7 @@
namespace Icinga\Module\Monitoring\Web\Menu;
use Icinga\Authentication\Manager;
use Icinga\Authentication\Auth;
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filterable;
use Icinga\Web\Menu;
@ -27,7 +27,7 @@ class MonitoringMenuItemRenderer extends MenuItemRenderer
protected static function applyRestriction($restriction, Filterable $filterable)
{
$restrictions = Filter::matchAny();
foreach (Manager::getInstance()->getRestrictions($restriction) as $filter) {
foreach (Auth::getInstance()->getRestrictions($restriction) as $filter) {
$restrictions->addFilter(Filter::fromQueryString($filter));
}
$filterable->applyFilter($restrictions);

View File

@ -19,7 +19,7 @@ $maxProgress = @max(array_keys(array_filter(
$notifications = Notification::getInstance();
if ($notifications->hasMessages()) {
foreach ($notifications->getMessages() as $m) {
foreach ($notifications->popMessages() as $m) {
echo '<li class="' . $m->type . '">' . $this->escape($m->message) . '</li>';
}
}
@ -135,4 +135,4 @@ if ($notifications->hasMessages()) {
<?= $this->render('index/parts/wizard.phtml'); ?>
<?php endif ?>
</div>
</div>
</div>