diff options
Diffstat (limited to 'library/Icinga/Authentication/AuthChain.php')
-rw-r--r-- | library/Icinga/Authentication/AuthChain.php | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/library/Icinga/Authentication/AuthChain.php b/library/Icinga/Authentication/AuthChain.php new file mode 100644 index 0000000..39468e3 --- /dev/null +++ b/library/Icinga/Authentication/AuthChain.php @@ -0,0 +1,269 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Authentication; + +use Icinga\Application\Hook\AuditHook; +use Iterator; +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 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 + */ + protected $config; + + /** + * The consecutive user backend while looping + * + * @var UserBackendInterface + */ + 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 = null) + { + 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) { + $user->setAdditional('backend_name', $backend->getName()); + $user->setAdditional('backend_type', $this->config->current()->get('backend')); + return true; + } + } + + if ($backendsTried === 0) { + $this->error = static::EEMPTY; + } elseif ($backendsTried === $backendsWithError) { + $this->error = static::EFAIL; + } elseif ($backendsWithError) { + $this->error = static::ENOTALL; + } else { + AuditHook::logActivity('login-failed', 'User failed to authenticate', null, $user->getUsername()); + } + + 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 void + */ + public function rewind(): void + { + $this->currentBackend = null; + $this->config->rewind(); + } + + /** + * Get the current user backend + * + * @return UserBackendInterface + */ + public function current(): UserBackendInterface + { + return $this->currentBackend; + } + + /** + * Get the key of the current user backend config + * + * @return string + */ + public function key(): string + { + return $this->config->key(); + } + + /** + * Move forward to the next user backend config + * + * @return void + */ + public function next(): void + { + $this->config->next(); + } + + /** + * 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 + */ + public function valid(): bool + { + 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)) { + $this->next(); + return $this->valid(); + } + + $name = $this->key(); + try { + $backend = UserBackend::create($name, $backendConfig); + } catch (ConfigurationError $e) { + Logger::error( + new ConfigurationError( + '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; + } +} |