summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Authentication/AuthChain.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Authentication/AuthChain.php')
-rw-r--r--library/Icinga/Authentication/AuthChain.php269
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;
+ }
+}