Merge pull request #2863 from Icinga/feature/domain-support-for-authn-authz-2153
This commit is contained in:
commit
686d022987
|
@ -5,12 +5,14 @@ namespace Icinga\Controllers;
|
|||
|
||||
use Exception;
|
||||
use Icinga\Application\Logger;
|
||||
use Icinga\Authentication\User\DomainAwareInterface;
|
||||
use Icinga\Data\DataArray\ArrayDatasource;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Data\Reducible;
|
||||
use Icinga\Exception\NotFoundError;
|
||||
use Icinga\Forms\Config\UserGroup\AddMemberForm;
|
||||
use Icinga\Forms\Config\UserGroup\UserGroupForm;
|
||||
use Icinga\User;
|
||||
use Icinga\Web\Controller\AuthBackendController;
|
||||
use Icinga\Web\Form;
|
||||
use Icinga\Web\Notification;
|
||||
|
@ -297,8 +299,27 @@ class GroupController extends AuthBackendController
|
|||
$users = array();
|
||||
foreach ($this->loadUserBackends('Icinga\Data\Selectable') as $backend) {
|
||||
try {
|
||||
foreach ($backend->select(array('user_name')) as $row) {
|
||||
$users[] = $row;
|
||||
if ($backend instanceof DomainAwareInterface) {
|
||||
$domain = $backend->getDomain();
|
||||
} else {
|
||||
$domain = null;
|
||||
}
|
||||
foreach ($backend->select(array('user_name')) as $user) {
|
||||
$userObj = new User($user->user_name);
|
||||
if ($domain !== null) {
|
||||
if ($userObj->hasDomain() && $userObj->getDomain() !== $domain) {
|
||||
// Users listed in a user backend which is configured to be responsible for a domain should
|
||||
// not have a domain in their username. Ultimately, if the username has a domain, it must
|
||||
// not differ from the backend's domain. We could log here - but hey, who cares :)
|
||||
continue;
|
||||
} else {
|
||||
$userObj->setDomain($domain);
|
||||
}
|
||||
}
|
||||
|
||||
$user->user_name = $userObj->getUsername();
|
||||
|
||||
$users[] = $user;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Logger::error($e);
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Icinga\Controllers;
|
|||
|
||||
use Exception;
|
||||
use Icinga\Application\Logger;
|
||||
use Icinga\Authentication\User\DomainAwareInterface;
|
||||
use Icinga\Data\DataArray\ArrayDatasource;
|
||||
use Icinga\Exception\ConfigurationError;
|
||||
use Icinga\Exception\NotFoundError;
|
||||
|
@ -96,7 +97,12 @@ class UserController extends AuthBackendController
|
|||
$this->httpNotFound(sprintf($this->translate('User "%s" not found'), $userName));
|
||||
}
|
||||
|
||||
$memberships = $this->loadMemberships(new User($userName))->select();
|
||||
$userObj = new User($userName);
|
||||
if ($backend instanceof DomainAwareInterface) {
|
||||
$userObj->setDomain($backend->getDomain());
|
||||
}
|
||||
|
||||
$memberships = $this->loadMemberships($userObj)->select();
|
||||
|
||||
$this->setupFilterControl(
|
||||
$memberships,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
namespace Icinga\Forms\Authentication;
|
||||
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Authentication\Auth;
|
||||
use Icinga\Authentication\User\ExternalBackend;
|
||||
use Icinga\User;
|
||||
|
@ -87,6 +88,9 @@ class LoginForm extends Form
|
|||
$authChain = $auth->getAuthChain();
|
||||
$authChain->setSkipExternalBackends(true);
|
||||
$user = new User($this->getElement('username')->getValue());
|
||||
if (! $user->hasDomain()) {
|
||||
$user->setDomain(Config::app()->get('authentication', 'default_domain'));
|
||||
}
|
||||
$password = $this->getElement('password')->getValue();
|
||||
$authenticated = $authChain->authenticate($user, $password);
|
||||
if ($authenticated) {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Icinga\Forms\Config\General;
|
||||
|
||||
use Icinga\Web\Form;
|
||||
|
||||
/**
|
||||
* Configuration form for the default domain for authentication
|
||||
*
|
||||
* This form is not used directly but as subform to the {@link GeneralConfigForm}.
|
||||
*/
|
||||
class DefaultAuthenticationDomainConfigForm extends Form
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$this->setName('form_config_general_authentication');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function createElements(array $formData)
|
||||
{
|
||||
$this->addElement(
|
||||
'text',
|
||||
'authentication_default_domain',
|
||||
array(
|
||||
'label' => $this->translate('Default Login Domain'),
|
||||
'description' => $this->translate(
|
||||
'If a user logs in without specifying any domain (e.g. "jdoe" instead of "jdoe@example.com"),'
|
||||
. ' this default domain will be assumed for the user. Note that if none your LDAP authentication'
|
||||
. ' backends are configured to be responsible for this domain or if none of your authentication'
|
||||
. ' backends holds usernames with the domain part, users will not be able to login.'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
namespace Icinga\Forms\Config;
|
||||
|
||||
use Icinga\Forms\Config\General\ApplicationConfigForm;
|
||||
use Icinga\Forms\Config\General\DefaultAuthenticationDomainConfigForm;
|
||||
use Icinga\Forms\Config\General\LoggingConfigForm;
|
||||
use Icinga\Forms\Config\General\ThemingConfigForm;
|
||||
use Icinga\Forms\ConfigForm;
|
||||
|
@ -30,8 +31,10 @@ class GeneralConfigForm extends ConfigForm
|
|||
$appConfigForm = new ApplicationConfigForm();
|
||||
$loggingConfigForm = new LoggingConfigForm();
|
||||
$themingConfigForm = new ThemingConfigForm();
|
||||
$domainConfigForm = new DefaultAuthenticationDomainConfigForm();
|
||||
$this->addSubForm($appConfigForm->create($formData));
|
||||
$this->addSubForm($loggingConfigForm->create($formData));
|
||||
$this->addSubForm($themingConfigForm->create($formData));
|
||||
$this->addSubForm($domainConfigForm->create($formData));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ namespace Icinga\Forms\Config\UserBackend;
|
|||
|
||||
use Exception;
|
||||
use Icinga\Data\ResourceFactory;
|
||||
use Icinga\Protocol\Ldap\LdapCapabilities;
|
||||
use Icinga\Protocol\Ldap\LdapConnection;
|
||||
use Icinga\Protocol\Ldap\LdapException;
|
||||
use Icinga\Web\Form;
|
||||
|
||||
/**
|
||||
|
@ -215,5 +218,115 @@ class LdapBackendForm extends Form
|
|||
'value' => $baseDn
|
||||
)
|
||||
);
|
||||
|
||||
$this->addElement(
|
||||
'text',
|
||||
'domain',
|
||||
array(
|
||||
'label' => $this->translate('Domain'),
|
||||
'description' => $this->translate(
|
||||
'The domain the LDAP server is responsible for upon authentication.'
|
||||
. ' Note that if you specify a domain here,'
|
||||
. ' the LDAP backend only authenticates users who specify a domain upon login.'
|
||||
. ' If the domain of the user matches the domain configured here, this backend is responsible for'
|
||||
. ' authenticating the user based on the username without the domain part.'
|
||||
. ' If your LDAP backend holds usernames with a domain part or if it is not necessary in your setup'
|
||||
. ' to authenticate users based on their domains, leave this field empty.'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$this->addElement(
|
||||
'button',
|
||||
'btn_discover_domain',
|
||||
array(
|
||||
'class' => 'control-button',
|
||||
'type' => 'submit',
|
||||
'value' => 'discovery_btn',
|
||||
'label' => $this->translate('Discover the domain'),
|
||||
'title' => $this->translate(
|
||||
'Push to disover and fill in the domain of the LDAP server.'
|
||||
),
|
||||
'decorators' => array(
|
||||
array('ViewHelper', array('separator' => '')),
|
||||
array('Spinner'),
|
||||
array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
|
||||
),
|
||||
'formnovalidate' => 'formnovalidate'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValidPartial(array $formData)
|
||||
{
|
||||
if (isset($formData['btn_discover_domain']) && parent::isValid($formData)) {
|
||||
return $this->populateDomain(ResourceFactory::create($this->getElement('resource')->getValue()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover the domain the LDAP server is responsible for and fill it in the form
|
||||
*
|
||||
* @param LdapConnection $connection
|
||||
*
|
||||
* @return bool Whether the discovery succeeded
|
||||
*/
|
||||
public function populateDomain(LdapConnection $connection)
|
||||
{
|
||||
try {
|
||||
$domain = $this->discoverDomain($connection);
|
||||
} catch (LdapException $e) {
|
||||
$this->_elements['btn_discover_domain']->addError($e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->_elements['domain']->setValue($domain);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover the domain the LDAP server is responsible for
|
||||
*
|
||||
* @param LdapConnection $connection
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function discoverDomain(LdapConnection $connection)
|
||||
{
|
||||
$cap = LdapCapabilities::discoverCapabilities($connection);
|
||||
|
||||
if ($cap->isActiveDirectory()) {
|
||||
$netBiosName = $cap->getNetBiosName();
|
||||
if ($netBiosName !== null) {
|
||||
return $netBiosName;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->defaultNamingContextToFQDN($cap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default naming context as FQDN
|
||||
*
|
||||
* @param LdapCapabilities $cap
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function defaultNamingContextToFQDN(LdapCapabilities $cap)
|
||||
{
|
||||
$defaultNamingContext = $cap->getDefaultNamingContext();
|
||||
if ($defaultNamingContext !== null) {
|
||||
$validationMatches = array();
|
||||
if (preg_match('/\bdc=[^,]+(?:,dc=[^,]+)*$/', strtolower($defaultNamingContext), $validationMatches)) {
|
||||
$splitMatches = array();
|
||||
preg_match_all('/dc=([^,]+)/', $validationMatches[0], $splitMatches);
|
||||
return implode('.', $splitMatches[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -390,6 +390,10 @@ class UserBackendConfigForm extends ConfigForm
|
|||
*/
|
||||
public function isValidPartial(array $formData)
|
||||
{
|
||||
if (! parent::isValidPartial($formData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getElement('backend_validation')->isChecked() && parent::isValid($formData)) {
|
||||
$inspection = static::inspectUserBackend($this);
|
||||
if ($inspection !== null) {
|
||||
|
|
|
@ -294,6 +294,16 @@ class LdapUserGroupBackendForm extends Form
|
|||
'value' => $defaults->user_base_dn
|
||||
)
|
||||
);
|
||||
$this->addElement(
|
||||
'text',
|
||||
'domain',
|
||||
array(
|
||||
'label' => $this->translate('Domain'),
|
||||
'description' => $this->translate(
|
||||
'The domain the LDAP server is responsible for.'
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -307,6 +317,7 @@ class LdapUserGroupBackendForm extends Form
|
|||
$this->addElement('hidden', 'user_filter', array('disabled' => true));
|
||||
$this->addElement('hidden', 'user_name_attribute', array('disabled' => true));
|
||||
$this->addElement('hidden', 'user_base_dn', array('disabled' => true));
|
||||
$this->addElement('hidden', 'domain', array('disabled' => true));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -150,3 +150,64 @@ Insert the user into the database using the generated password hash:
|
|||
```
|
||||
INSERT INTO icingaweb_user (name, active, password_hash) VALUES ('icingaadmin', 1, 'hash from openssl');
|
||||
```
|
||||
|
||||
## <a id="domain-aware-auth"></a> Domain-aware Authentication
|
||||
|
||||
If there are multiple LDAP/AD authentication backends with distinct domains, you should make Icinga Web 2 aware of the
|
||||
domains. This is possible since version 2.5 and can be done by configuring each LDAP/AD backend's domain. You can also
|
||||
use the GUI for this purpose. This enables you to automatically discover a suitable value based on your LDAP server's
|
||||
configuration. (AD: NetBIOS name, other LDAP: domain in DNS-notation)
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
[auth_icinga]
|
||||
backend = ldap
|
||||
resource = icinga_ldap
|
||||
user_class = inetOrgPerson
|
||||
user_name_attribute = uid
|
||||
filter = "memberOf=cn=icinga_users,cn=groups,cn=accounts,dc=icinga,dc=com"
|
||||
domain = "icinga.com"
|
||||
|
||||
[auth_example]
|
||||
backend = msldap
|
||||
resource = example_ad
|
||||
domain = EXAMPLE
|
||||
```
|
||||
|
||||
If you configure the domains like above, the icinga.com user "jdoe" will have to log in as "jdoe@icinga.com" and the
|
||||
EXAMPLE employee "rroe" will have to log in as "rroe@EXAMPLE". They could also log in as "EXAMPLE\\rroe", but this gets
|
||||
converted to "rroe@EXAMPLE" as soon as the user logs in.
|
||||
|
||||
**Caution!**
|
||||
|
||||
Enabling domain-awareness or changing domains in existing setups requires migration of the usernames in the Icinga Web 2
|
||||
configuration. Consult `icingacli --help migrate config users` for details.
|
||||
|
||||
### <a id="default-auth-domain"></a> Default Domain
|
||||
|
||||
For the sake of simplicity a default domain can be configured (in `config.ini`).
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
[authentication]
|
||||
default_domain = "icinga.com"
|
||||
```
|
||||
|
||||
If you configure the default domain like above, the user "jdoe@icinga.com" will be able to just type "jdoe" as username
|
||||
while logging in.
|
||||
|
||||
### <a id="domain-aware-auth-process"></a> How it works
|
||||
|
||||
### <a id="domain-aware-auth-ad"></a> Active Directory
|
||||
|
||||
When the user "jdoe@ICINGA" logs in, Icinga Web 2 walks through all configured authentication backends until it finds
|
||||
one which is responsible for that user -- e.g. an Active Directory backend with the domain "ICINGA". Then Icinga Web 2
|
||||
asks that backend to authenticate the user with the sAMAccountName "jdoe".
|
||||
|
||||
### <a id="domain-aware-auth-sqldb"></a> SQL Database
|
||||
|
||||
When the user "jdoe@icinga.com" logs in, Icinga Web 2 walks through all configured authentication backends until it
|
||||
finds one which is responsible for that user -- e.g. a MySQL backend (SQL database backends aren't domain-aware). Then
|
||||
Icinga Web 2 asks that backend to authenticate the user with the username "jdoe@icinga.com".
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+
|
||||
|
||||
ALTER TABLE `icingaweb_group_membership` MODIFY COLUMN `username` varchar(254) COLLATE utf8_unicode_ci NOT NULL;
|
||||
ALTER TABLE `icingaweb_user` MODIFY COLUMN `name` varchar(254) COLLATE utf8_unicode_ci NOT NULL;
|
||||
ALTER TABLE `icingaweb_user_preference` MODIFY COLUMN `username` varchar(254) COLLATE utf8_unicode_ci NOT NULL;
|
|
@ -14,7 +14,7 @@ CREATE TABLE `icingaweb_group`(
|
|||
|
||||
CREATE TABLE `icingaweb_group_membership`(
|
||||
`group_id` int(10) unsigned NOT NULL,
|
||||
`username` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`username` varchar(254) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`ctime` timestamp NULL DEFAULT NULL,
|
||||
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`group_id`,`username`),
|
||||
|
@ -23,7 +23,7 @@ CREATE TABLE `icingaweb_group_membership`(
|
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE `icingaweb_user`(
|
||||
`name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`name` varchar(254) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`active` tinyint(1) NOT NULL,
|
||||
`password_hash` varbinary(255) NOT NULL,
|
||||
`ctime` timestamp NULL DEFAULT NULL,
|
||||
|
@ -32,7 +32,7 @@ CREATE TABLE `icingaweb_user`(
|
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE `icingaweb_user_preference`(
|
||||
`username` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`username` varchar(254) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`section` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`value` varchar(255) NOT NULL,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
|
||||
|
||||
ALTER TABLE "icingaweb_group_membership" ALTER COLUMN "username" TYPE character varying(254);
|
||||
ALTER TABLE "icingaweb_user" ALTER COLUMN "name" TYPE character varying(254);
|
||||
ALTER TABLE "icingaweb_user_preference" ALTER COLUMN "username" TYPE character varying(254);
|
|
@ -35,7 +35,7 @@ ALTER TABLE ONLY "icingaweb_group"
|
|||
|
||||
CREATE TABLE "icingaweb_group_membership" (
|
||||
"group_id" int NOT NULL,
|
||||
"username" character varying(64) NOT NULL,
|
||||
"username" character varying(254) NOT NULL,
|
||||
"ctime" timestamp NULL DEFAULT NULL,
|
||||
"mtime" timestamp NULL DEFAULT NULL
|
||||
);
|
||||
|
@ -57,7 +57,7 @@ CREATE UNIQUE INDEX idx_icingaweb_group_membership
|
|||
);
|
||||
|
||||
CREATE TABLE "icingaweb_user" (
|
||||
"name" character varying(64) NOT NULL,
|
||||
"name" character varying(254) NOT NULL,
|
||||
"active" smallint NOT NULL,
|
||||
"password_hash" bytea NOT NULL,
|
||||
"ctime" timestamp NULL DEFAULT NULL,
|
||||
|
@ -77,7 +77,7 @@ CREATE UNIQUE INDEX idx_icingaweb_user
|
|||
);
|
||||
|
||||
CREATE TABLE "icingaweb_user_preference" (
|
||||
"username" character varying(64) NOT NULL,
|
||||
"username" character varying(254) NOT NULL,
|
||||
"name" character varying(64) NOT NULL,
|
||||
"section" character varying(64) NOT NULL,
|
||||
"value" character varying(255) NOT NULL,
|
||||
|
|
|
@ -259,6 +259,9 @@ class Auth
|
|||
foreach ($this->getAuthChain() as $userBackend) {
|
||||
if ($userBackend instanceof ExternalBackend) {
|
||||
if ($userBackend->authenticate($user)) {
|
||||
if (! $user->hasDomain()) {
|
||||
$user->setDomain(Config::app()->get('authentication', 'default_domain'));
|
||||
}
|
||||
$this->setAuthenticated($user);
|
||||
return true;
|
||||
}
|
||||
|
@ -293,6 +296,9 @@ class Auth
|
|||
return false;
|
||||
}
|
||||
$user = new User($credentials[0]);
|
||||
if (! $user->hasDomain()) {
|
||||
$user->setDomain(Config::app()->get('authentication', 'default_domain'));
|
||||
}
|
||||
$password = $credentials[1];
|
||||
if ($this->getAuthChain()->setSkipExternalBackends(true)->authenticate($user, $password)) {
|
||||
$this->setAuthenticated($user, false);
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Icinga\Authentication\User;
|
||||
|
||||
/**
|
||||
* Interface for user backends that are responsible for a specific domain
|
||||
*/
|
||||
interface DomainAwareInterface
|
||||
{
|
||||
/**
|
||||
* Get the domain the backend is responsible for
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDomain();
|
||||
}
|
|
@ -14,7 +14,7 @@ use Icinga\Repository\RepositoryQuery;
|
|||
use Icinga\Protocol\Ldap\LdapException;
|
||||
use Icinga\User;
|
||||
|
||||
class LdapUserBackend extends LdapRepository implements UserBackendInterface, Inspectable
|
||||
class LdapUserBackend extends LdapRepository implements UserBackendInterface, DomainAwareInterface, Inspectable
|
||||
{
|
||||
/**
|
||||
* The base DN to use for a query
|
||||
|
@ -44,6 +44,13 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
|
|||
*/
|
||||
protected $filter;
|
||||
|
||||
/**
|
||||
* The domain the backend is responsible for
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $domain;
|
||||
|
||||
/**
|
||||
* The columns which are not permitted to be queried
|
||||
*
|
||||
|
@ -174,6 +181,29 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
|
|||
return $this->filter;
|
||||
}
|
||||
|
||||
public function getDomain()
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the domain the backend is responsible for
|
||||
*
|
||||
* @param string $domain
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDomain($domain)
|
||||
{
|
||||
$domain = trim($domain);
|
||||
|
||||
if (strlen($domain)) {
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given configuration to this backend
|
||||
*
|
||||
|
@ -187,7 +217,8 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
|
|||
->setBaseDn($config->base_dn)
|
||||
->setUserClass($config->user_class)
|
||||
->setUserNameAttribute($config->user_name_attribute)
|
||||
->setFilter($config->filter);
|
||||
->setFilter($config->filter)
|
||||
->setDomain($config->domain);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -372,10 +403,20 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
|
|||
*/
|
||||
public function authenticate(User $user, $password)
|
||||
{
|
||||
if ($this->domain !== null) {
|
||||
if (! $user->hasDomain() || strtolower($user->getDomain()) !== $this->domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$username = $user->getLocalUsername();
|
||||
} else {
|
||||
$username = $user->getUsername();
|
||||
}
|
||||
|
||||
try {
|
||||
$userDn = $this
|
||||
->select()
|
||||
->where('user_name', str_replace('*', '', $user->getUsername()))
|
||||
->where('user_name', str_replace('*', '', $username))
|
||||
->getQuery()
|
||||
->setUsePagedResults(false)
|
||||
->fetchDn();
|
||||
|
@ -392,7 +433,7 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
|
|||
} catch (LdapException $e) {
|
||||
throw new AuthenticationException(
|
||||
'Failed to authenticate user "%s" against backend "%s". An exception was thrown:',
|
||||
$user->getUsername(),
|
||||
$username,
|
||||
$this->getName(),
|
||||
$e
|
||||
);
|
||||
|
|
|
@ -231,4 +231,55 @@ class UserBackend implements ConfigAwareFactory
|
|||
$backend->setName($name);
|
||||
return $backend;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the given backend is responsible for authenticating the given user (based on their domains)
|
||||
*
|
||||
* @param UserBackendInterface $backend
|
||||
* @param User $user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isBackendResponsibleForUser(UserBackendInterface $backend, User $user)
|
||||
{
|
||||
$backendDomain = static::getBackendDomain($backend);
|
||||
$userDomain = $user->getDomain();
|
||||
|
||||
if ($userDomain === null) {
|
||||
// The user logs in as "jdoe", not as "jdoe@example.com" and there's no default domain.
|
||||
// The backend is only responsible if its domain is also missing.
|
||||
return $backendDomain === null;
|
||||
} else {
|
||||
// The user logs in as "jdoe@example.com" or "jdoe" with a default domain being configured.
|
||||
return strtolower($userDomain) === strtolower($backendDomain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the domain the given backend is responsible for (fall back to the default domain if any)
|
||||
*
|
||||
* @param UserBackendInterface $backend
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getBackendDomain(UserBackendInterface $backend)
|
||||
{
|
||||
$backendDomain = Config::app('authentication')->get($backend->getName(), 'domain');
|
||||
return $backendDomain === null ? Config::app()->get('authentication', 'default_domain') : $backendDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user from the given username without its domain and the backend as fully assembled {@link User} object
|
||||
*
|
||||
* @param string $localUsername
|
||||
* @param UserBackendInterface $backend
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public static function getUserFromBackend($localUsername, UserBackendInterface $backend)
|
||||
{
|
||||
$user = new User($localUsername);
|
||||
$user->setDomain(static::getBackendDomain($backend));
|
||||
return $user->setDefaultDomainIfNeeded();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,13 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
|
|||
*/
|
||||
protected $nestedGroupSearch;
|
||||
|
||||
/**
|
||||
* The domain the backend is responsible for
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $domain;
|
||||
|
||||
/**
|
||||
* The columns which are not permitted to be queried
|
||||
*
|
||||
|
@ -394,6 +401,40 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
|
|||
return $this->nestedGroupSearch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the domain the backend is responsible for
|
||||
*
|
||||
* If the LDAP group backend is linked with a LDAP user backend,
|
||||
* the domain of the user backend will be returned.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDomain()
|
||||
{
|
||||
return $this->userBackend !== null ? $this->userBackend->getDomain() : $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the domain the backend is responsible for
|
||||
*
|
||||
* If the LDAP group backend is linked with a LDAP user backend,
|
||||
* the domain of the user backend will be used nonetheless.
|
||||
*
|
||||
* @param string $domain
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDomain($domain)
|
||||
{
|
||||
$domain = trim($domain);
|
||||
|
||||
if (strlen($domain)) {
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the attribute name where to find a group's member holds ambiguous values
|
||||
*
|
||||
|
@ -632,13 +673,25 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
|
|||
*/
|
||||
public function getMemberships(User $user)
|
||||
{
|
||||
$domain = $this->getDomain();
|
||||
|
||||
if ($domain !== null) {
|
||||
if (! $user->hasDomain() || strtolower($user->getDomain()) !== $domain) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$username = $user->getLocalUsername();
|
||||
} else {
|
||||
$username = $user->getUsername();
|
||||
}
|
||||
|
||||
if ($this->isMemberAttributeAmbiguous()) {
|
||||
$queryValue = $user->getUsername();
|
||||
$queryValue = $username;
|
||||
} elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) {
|
||||
$userQuery = $this->ds
|
||||
->select()
|
||||
->from($this->userClass)
|
||||
->where($this->userNameAttribute, $user->getUsername())
|
||||
->where($this->userNameAttribute, $username)
|
||||
->setBase($this->userBaseDn)
|
||||
->setUsePagedResults(false);
|
||||
if ($this->userFilter) {
|
||||
|
@ -742,7 +795,8 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
|
|||
->setGroupMemberAttribute($config->get('group_member_attribute', $defaults->group_member_attribute))
|
||||
->setGroupFilter($config->group_filter)
|
||||
->setUserFilter($config->user_filter)
|
||||
->setNestedGroupSearch((bool) $config->get('nested_group_search', $defaults->nested_group_search));
|
||||
->setNestedGroupSearch((bool) $config->get('nested_group_search', $defaults->nested_group_search))
|
||||
->setDomain($config->domain);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -90,7 +90,7 @@ class LdapCapabilities
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
private $oids = array();
|
||||
private $oids;
|
||||
|
||||
/**
|
||||
* Construct a new capability
|
||||
|
@ -98,8 +98,19 @@ class LdapCapabilities
|
|||
* @param $attributes StdClass The attributes returned, may be null for guessing default capabilities
|
||||
*/
|
||||
public function __construct($attributes = null)
|
||||
{
|
||||
$this->setAttributes($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the attributes and (re)build the OIDs
|
||||
*
|
||||
* @param $attributes StdClass The attributes returned, may be null for guessing default capabilities
|
||||
*/
|
||||
protected function setAttributes($attributes)
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
$this->oids = array();
|
||||
|
||||
$keys = array('supportedControl', 'supportedExtension', 'supportedFeatures', 'supportedCapabilities');
|
||||
foreach ($keys as $key) {
|
||||
|
@ -203,6 +214,30 @@ class LdapCapabilities
|
|||
return empty($namingContexts) ? null : $namingContexts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration naming context
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getConfigurationNamingContext()
|
||||
{
|
||||
if (isset($this->attributes->configurationNamingContext)) {
|
||||
return $this->attributes->configurationNamingContext;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the NetBIOS name
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getNetBiosName()
|
||||
{
|
||||
if (isset($this->attributes->nETBIOSName)) {
|
||||
return $this->attributes->nETBIOSName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the namingContexts
|
||||
*
|
||||
|
@ -273,6 +308,7 @@ class LdapCapabilities
|
|||
$ds = $connection->getConnection();
|
||||
|
||||
$fields = array(
|
||||
'configurationNamingContext',
|
||||
'defaultNamingContext',
|
||||
'namingContexts',
|
||||
'vendorName',
|
||||
|
@ -310,9 +346,65 @@ class LdapCapabilities
|
|||
}
|
||||
|
||||
$cap = new LdapCapabilities($connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $fields));
|
||||
$cap->discoverAdConfigOptions($connection);
|
||||
return $cap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover the AD-specific configuration options of the given LDAP server
|
||||
*
|
||||
* @param LdapConnection $connection The ldap connection to use
|
||||
*
|
||||
* @throws LdapException In case the configuration options query has failed
|
||||
*/
|
||||
protected function discoverAdConfigOptions(LdapConnection $connection)
|
||||
{
|
||||
if ($this->isActiveDirectory()) {
|
||||
$configurationNamingContext = $this->getConfigurationNamingContext();
|
||||
$defaultNamingContext = $this->getDefaultNamingContext();
|
||||
if (!($configurationNamingContext === null || $defaultNamingContext === null)) {
|
||||
$ds = $connection->bind()->getConnection();
|
||||
$adFields = array('nETBIOSName');
|
||||
$partitions = 'CN=Partitions,' . $configurationNamingContext;
|
||||
|
||||
$result = @ldap_list(
|
||||
$ds,
|
||||
$partitions,
|
||||
(string) $connection->select()->from('*', $adFields)->where('nCName', $defaultNamingContext),
|
||||
$adFields
|
||||
);
|
||||
if ($result) {
|
||||
$entry = ldap_first_entry($ds, $result);
|
||||
if ($entry === false) {
|
||||
throw new LdapException(
|
||||
'Configuration options not available (%s:%d). Discovery of "%s" probably not permitted.',
|
||||
$connection->getHostname(),
|
||||
$connection->getPort(),
|
||||
$partitions
|
||||
);
|
||||
}
|
||||
|
||||
$this->setAttributes((object) array_merge(
|
||||
(array) $this->attributes,
|
||||
(array) $connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $adFields)
|
||||
));
|
||||
} else {
|
||||
if (ldap_errno($ds) !== 1) {
|
||||
// One stands for "operations error" which occurs if not bound non-anonymously.
|
||||
|
||||
throw new LdapException(
|
||||
'Configuration options query failed (%s:%d): %s. Check if hostname and port of the'
|
||||
. ' ldap resource are correct and if anonymous access is permitted.',
|
||||
$connection->getHostname(),
|
||||
$connection->getPort(),
|
||||
ldap_error($ds)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the active directory version using the available capabillities
|
||||
*
|
||||
|
|
|
@ -7,6 +7,7 @@ use DateTimeZone;
|
|||
use InvalidArgumentException;
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Authentication\Role;
|
||||
use Icinga\Exception\ProgrammingError;
|
||||
use Icinga\User\Preferences;
|
||||
use Icinga\Web\Navigation\Navigation;
|
||||
|
||||
|
@ -17,13 +18,6 @@ use Icinga\Web\Navigation\Navigation;
|
|||
*/
|
||||
class User
|
||||
{
|
||||
/**
|
||||
* Username
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $username;
|
||||
|
||||
/**
|
||||
* Firstname
|
||||
*
|
||||
|
@ -45,6 +39,13 @@ class User
|
|||
*/
|
||||
protected $email;
|
||||
|
||||
/**
|
||||
* {@link username} without {@link domain}
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $localUsername;
|
||||
|
||||
/**
|
||||
* Domain
|
||||
*
|
||||
|
@ -279,7 +280,7 @@ class User
|
|||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->username;
|
||||
return $this->domain === null ? $this->localUsername : $this->localUsername . '@' . $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -289,7 +290,18 @@ class User
|
|||
*/
|
||||
public function setUsername($name)
|
||||
{
|
||||
$this->username = $name;
|
||||
$parts = explode('\\', $name, 2);
|
||||
if (count($parts) === 2) {
|
||||
list($this->domain, $this->localUsername) = $parts;
|
||||
} else {
|
||||
$parts = explode('@', $name, 2);
|
||||
if (count($parts) === 2) {
|
||||
list($this->localUsername, $this->domain) = $parts;
|
||||
} else {
|
||||
$this->localUsername = $name;
|
||||
$this->domain = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -354,30 +366,61 @@ class User
|
|||
if (filter_var($mail, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->email = $mail;
|
||||
} else {
|
||||
throw new InvalidArgumentException("Invalid mail given for user $this->username: $mail");
|
||||
throw new InvalidArgumentException('Invalid mail given for user ' . $this->getUsername() . ': $mail');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for domain
|
||||
* Set the domain
|
||||
*
|
||||
* @param string $domain
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDomain($domain)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for domain
|
||||
* Get whether the user has a domain
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDomain()
|
||||
{
|
||||
return $this->domain !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the domain
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws ProgrammingError If the user does not have a domain
|
||||
*/
|
||||
public function getDomain()
|
||||
{
|
||||
if ($this->domain === null) {
|
||||
throw new ProgrammingError(
|
||||
'User does not have a domain.'
|
||||
. ' Use User::hasDomain() to check whether the user has a domain beforehand.'
|
||||
);
|
||||
}
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the local username, ie. the username without its domain
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocalUsername()
|
||||
{
|
||||
return $this->localUsername;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set additional information about user
|
||||
|
|
|
@ -13,12 +13,19 @@ class StringHelper
|
|||
*
|
||||
* @param string $value
|
||||
* @param string $delimiter
|
||||
* @param int $limit
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function trimSplit($value, $delimiter = ',')
|
||||
public static function trimSplit($value, $delimiter = ',', $limit = null)
|
||||
{
|
||||
return array_map('trim', explode($delimiter, $value));
|
||||
if ($limit !== null) {
|
||||
$exploded = explode($delimiter, $value, $limit);
|
||||
} else {
|
||||
$exploded = explode($delimiter, $value);
|
||||
}
|
||||
|
||||
return array_map('trim', $exploded);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Icinga\Module\Migrate\Clicommands;
|
||||
|
||||
use Icinga\Cli\Command;
|
||||
use Icinga\Module\Migrate\Config\UserDomainMigration;
|
||||
use Icinga\User;
|
||||
use Icinga\Util\StringHelper;
|
||||
|
||||
class ConfigCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Rename users and user configurations according to a given domain
|
||||
*
|
||||
* The following configurations are taken into account:
|
||||
* - Announcements
|
||||
* - Preferences
|
||||
* - Dashboards
|
||||
* - Custom navigation items
|
||||
* - Role configuration
|
||||
* - Users and group memberships in database backends, if configured
|
||||
*
|
||||
* USAGE:
|
||||
*
|
||||
* icingacli migrate config users [options]
|
||||
*
|
||||
* OPTIONS:
|
||||
*
|
||||
* --to-domain=<to-domain> The new domain for the users
|
||||
*
|
||||
* --from-domain=<from-domain> Migrate only the users with the given domain.
|
||||
* Use this switch in combination with --to-domain.
|
||||
*
|
||||
* --user=<user> Migrate only the given user in the format <user> or <user@domain>
|
||||
*
|
||||
* --map-file=<mapfile> File to use for renaming users
|
||||
*
|
||||
* --separator=<separator> Separator for the map file
|
||||
*
|
||||
* EXAMPLES:
|
||||
*
|
||||
* icingacli migrate config users ...
|
||||
*
|
||||
* Add the domain "icinga.com" to all users:
|
||||
*
|
||||
* --to-domain icinga.com
|
||||
*
|
||||
* Set the domain "example.com" on all users that have the domain "icinga.com":
|
||||
*
|
||||
* --to-domain example.com --from-domain icinga.com
|
||||
*
|
||||
* Set the domain "icinga.com" on the user "icingaadmin":
|
||||
*
|
||||
* --to-domain icinga.com --user icingaadmin
|
||||
*
|
||||
* Set the domain "icinga.com" on the users "icingaadmin@icinga.com"
|
||||
*
|
||||
* --to-domain example.com --user icingaadmin@icinga.com
|
||||
*
|
||||
* Rename users according to a map file:
|
||||
*
|
||||
* --map-file /path/to/mapfile --separator :
|
||||
*
|
||||
* MAPFILE:
|
||||
*
|
||||
* You may rename users according to a given map file. The map file must be separated by newlines. Each line then
|
||||
* is specified in the format <from><separator><to>. The separator is specified with the --separator switch.
|
||||
*
|
||||
* Example content:
|
||||
*
|
||||
* icingaadmin:icingaadmin@icinga.com
|
||||
* jdoe@example.com:jdoe@icinga.com
|
||||
* rroe@icinga:rroe@icinga.com
|
||||
*/
|
||||
public function usersAction()
|
||||
{
|
||||
if ($this->params->has('map-file')) {
|
||||
$mapFile = $this->params->get('map-file');
|
||||
$separator = $this->params->getRequired('separator');
|
||||
|
||||
$source = trim(file_get_contents($mapFile));
|
||||
$source = StringHelper::trimSplit($source, "\n");
|
||||
|
||||
$map = array();
|
||||
|
||||
array_walk($source, function ($item) use ($separator, &$map) {
|
||||
list($from, $to) = StringHelper::trimSplit($item, $separator, 2);
|
||||
$map[$from] = $to;
|
||||
});
|
||||
|
||||
$migration = UserDomainMigration::fromMap($map);
|
||||
} else {
|
||||
$toDomain = $this->params->getRequired('to-domain');
|
||||
$fromDomain = $this->params->get('from-domain');
|
||||
$user = $this->params->get('user');
|
||||
|
||||
if ($user === null) {
|
||||
$migration = UserDomainMigration::fromDomains($toDomain, $fromDomain);
|
||||
} else {
|
||||
if ($fromDomain !== null) {
|
||||
$this->fail(
|
||||
"Ambiguous arguments: Can't use --user in combination with --from-domain."
|
||||
. " Please use the user@domain syntax for the --user switch instead."
|
||||
);
|
||||
}
|
||||
|
||||
$user = new User($user);
|
||||
|
||||
$migrated = clone $user;
|
||||
$migrated->setDomain($toDomain);
|
||||
|
||||
$migration = UserDomainMigration::fromMap(array($user->getUsername() => $migrated->getUsername()));
|
||||
}
|
||||
}
|
||||
|
||||
$migration->migrate();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,393 @@
|
|||
<?php
|
||||
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
|
||||
|
||||
namespace Icinga\Module\Migrate\Config;
|
||||
|
||||
use Icinga\Application\Config;
|
||||
use Icinga\Data\Db\DbConnection;
|
||||
use Icinga\Data\Filter\Filter;
|
||||
use Icinga\Data\ResourceFactory;
|
||||
use Icinga\User;
|
||||
use Icinga\Util\DirectoryIterator;
|
||||
use Icinga\Util\StringHelper;
|
||||
use Icinga\Web\Announcement\AnnouncementIniRepository;
|
||||
|
||||
class UserDomainMigration
|
||||
{
|
||||
protected $toDomain;
|
||||
|
||||
protected $fromDomain;
|
||||
|
||||
protected $map;
|
||||
|
||||
public static function fromMap(array $map)
|
||||
{
|
||||
$static = new static();
|
||||
|
||||
$static->map = $map;
|
||||
|
||||
return $static;
|
||||
}
|
||||
|
||||
public static function fromDomains($toDomain, $fromDomain = null)
|
||||
{
|
||||
$static = new static();
|
||||
|
||||
$static->toDomain = $toDomain;
|
||||
$static->fromDomain = $fromDomain;
|
||||
|
||||
return $static;
|
||||
}
|
||||
|
||||
protected function mustMigrate(User $user)
|
||||
{
|
||||
if ($user->getUsername() === '*') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->map !== null) {
|
||||
return isset($this->map[$user->getUsername()]);
|
||||
}
|
||||
|
||||
if ($this->fromDomain !== null && $user->hasDomain() && $user->getDomain() !== $this->fromDomain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function migrateUser(User $user)
|
||||
{
|
||||
$migrated = clone $user;
|
||||
|
||||
if ($this->map !== null) {
|
||||
$migrated->setUsername($this->map[$user->getUsername()]);
|
||||
} else {
|
||||
$migrated->setDomain($this->toDomain);
|
||||
}
|
||||
|
||||
return $migrated;
|
||||
}
|
||||
|
||||
protected function migrateAnnounces()
|
||||
{
|
||||
$announces = new AnnouncementIniRepository();
|
||||
|
||||
$query = $announces->select(array('author'));
|
||||
|
||||
if ($this->map !== null) {
|
||||
$query->where('author', array_keys($this->map));
|
||||
}
|
||||
|
||||
$migratedUsers = array();
|
||||
|
||||
foreach ($announces->select(array('author')) as $announce) {
|
||||
$user = new User($announce->author);
|
||||
|
||||
if (! $this->mustMigrate($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($migratedUsers[$user->getUsername()])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$announces->update(
|
||||
'announcement',
|
||||
array('author' => $migrated->getUsername()),
|
||||
Filter::where('author', $user->getUsername())
|
||||
);
|
||||
|
||||
$migratedUsers[$user->getUsername()] = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function migrateDashboards()
|
||||
{
|
||||
$directory = Config::resolvePath('dashboards');
|
||||
|
||||
$migration = array();
|
||||
|
||||
if (DirectoryIterator::isReadable($directory)) {
|
||||
foreach (new DirectoryIterator($directory) as $username => $path) {
|
||||
$user = new User($username);
|
||||
|
||||
if (! $this->mustMigrate($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$migration[$path] = dirname($path) . '/' . $migrated->getUsername();
|
||||
}
|
||||
|
||||
foreach ($migration as $from => $to) {
|
||||
rename($from, $to);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected function migrateNavigation()
|
||||
{
|
||||
$directory = Config::resolvePath('navigation');
|
||||
|
||||
foreach (new DirectoryIterator($directory, 'ini') as $file) {
|
||||
$config = Config::fromIni($file);
|
||||
|
||||
foreach ($config as $navigation) {
|
||||
$owner = $navigation->owner;
|
||||
|
||||
if (! empty($owner)) {
|
||||
$user = new User($owner);
|
||||
|
||||
if ($this->mustMigrate($user)) {
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$navigation->owner = $migrated->getUsername();
|
||||
}
|
||||
}
|
||||
|
||||
$users = $navigation->users;
|
||||
|
||||
if (! empty($users)) {
|
||||
$users = StringHelper::trimSplit($users);
|
||||
|
||||
foreach ($users as &$username) {
|
||||
$user = new User($username);
|
||||
|
||||
if (! $this->mustMigrate($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$username = $migrated->getUsername();
|
||||
}
|
||||
|
||||
$navigation->users = implode(',', $users);
|
||||
}
|
||||
}
|
||||
|
||||
$config->saveIni();
|
||||
}
|
||||
}
|
||||
|
||||
protected function migratePreferences()
|
||||
{
|
||||
$config = Config::app();
|
||||
|
||||
$type = $config->get('global', 'config_backend', 'ini');
|
||||
|
||||
switch ($type) {
|
||||
case 'ini':
|
||||
$directory = Config::resolvePath('preferences');
|
||||
|
||||
$migration = array();
|
||||
|
||||
if (DirectoryIterator::isReadable($directory)) {
|
||||
foreach (new DirectoryIterator($directory) as $username => $path) {
|
||||
$user = new User($username);
|
||||
|
||||
if (! $this->mustMigrate($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$migration[$path] = dirname($path) . '/' . $migrated->getUsername();
|
||||
}
|
||||
|
||||
foreach ($migration as $from => $to) {
|
||||
rename($from, $to);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'db':
|
||||
/** @var DbConnection $conn */
|
||||
$conn = ResourceFactory::create($config->get('global', 'config_resource'));
|
||||
|
||||
$query = $conn
|
||||
->select()
|
||||
->from('icingaweb_user_preference', array('username'))
|
||||
->group('username');
|
||||
|
||||
if ($this->map !== null) {
|
||||
$query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map))));
|
||||
}
|
||||
|
||||
$users = $query->fetchColumn();
|
||||
|
||||
$migration = array();
|
||||
|
||||
foreach ($users as $username) {
|
||||
$user = new User($username);
|
||||
|
||||
if (! $this->mustMigrate($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$migration[$username] = $migrated->getUsername();
|
||||
}
|
||||
|
||||
if (! empty($migration)) {
|
||||
$conn->getDbAdapter()->beginTransaction();
|
||||
|
||||
foreach ($migration as $originalUsername => $username) {
|
||||
$conn->update(
|
||||
'icingaweb_user_preference',
|
||||
array('username' => $username),
|
||||
Filter::where('username', $originalUsername)
|
||||
);
|
||||
}
|
||||
|
||||
$conn->getDbAdapter()->commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function migrateRoles()
|
||||
{
|
||||
$roles = Config::app('roles');
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$users = $role->users;
|
||||
|
||||
if (empty($users)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$users = StringHelper::trimSplit($users);
|
||||
|
||||
foreach ($users as &$username) {
|
||||
$user = new User($username);
|
||||
|
||||
if (! $this->mustMigrate($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$username = $migrated->getUsername();
|
||||
}
|
||||
|
||||
$role->users = implode(',', $users);
|
||||
}
|
||||
|
||||
$roles->saveIni();
|
||||
}
|
||||
|
||||
protected function migrateUsers()
|
||||
{
|
||||
foreach (Config::app('authentication') as $name => $config) {
|
||||
if (strtolower($config->backend) !== 'db') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var DbConnection $conn */
|
||||
$conn = ResourceFactory::create($config->resource);
|
||||
|
||||
$query = $conn
|
||||
->select()
|
||||
->from('icingaweb_user', array('name'))
|
||||
->group('name');
|
||||
|
||||
if ($this->map !== null) {
|
||||
$query->applyFilter(Filter::matchAny(Filter::where('name', array_keys($this->map))));
|
||||
}
|
||||
|
||||
$users = $query->fetchColumn();
|
||||
|
||||
$migration = array();
|
||||
|
||||
foreach ($users as $username) {
|
||||
$user = new User($username);
|
||||
|
||||
if (! $this->mustMigrate($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$migration[$username] = $migrated->getUsername();
|
||||
}
|
||||
|
||||
if (! empty($migration)) {
|
||||
$conn->getDbAdapter()->beginTransaction();
|
||||
|
||||
foreach ($migration as $originalUsername => $username) {
|
||||
$conn->update(
|
||||
'icingaweb_user',
|
||||
array('name' => $username),
|
||||
Filter::where('name', $originalUsername)
|
||||
);
|
||||
}
|
||||
|
||||
$conn->getDbAdapter()->commit();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Config::app('groups') as $name => $config) {
|
||||
if (strtolower($config->backend) !== 'db') {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var DbConnection $conn */
|
||||
$conn = ResourceFactory::create($config->resource);
|
||||
|
||||
$query = $conn
|
||||
->select()
|
||||
->from('icingaweb_group_membership', array('username'))
|
||||
->group('username');
|
||||
|
||||
if ($this->map !== null) {
|
||||
$query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map))));
|
||||
}
|
||||
|
||||
$users = $query->fetchColumn();
|
||||
|
||||
$migration = array();
|
||||
|
||||
foreach ($users as $username) {
|
||||
$user = new User($username);
|
||||
|
||||
if (! $this->mustMigrate($user)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$migrated = $this->migrateUser($user);
|
||||
|
||||
$migration[$username] = $migrated->getUsername();
|
||||
}
|
||||
|
||||
if (! empty($migration)) {
|
||||
$conn->getDbAdapter()->beginTransaction();
|
||||
|
||||
foreach ($migration as $originalUsername => $username) {
|
||||
$conn->update(
|
||||
'icingaweb_group_membership',
|
||||
array('username' => $username),
|
||||
Filter::where('username', $originalUsername)
|
||||
);
|
||||
}
|
||||
|
||||
$conn->getDbAdapter()->commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function migrate()
|
||||
{
|
||||
$this->migrateAnnounces();
|
||||
$this->migrateDashboards();
|
||||
$this->migrateNavigation();
|
||||
$this->migratePreferences();
|
||||
$this->migrateRoles();
|
||||
$this->migrateUsers();
|
||||
}
|
||||
}
|
|
@ -204,6 +204,8 @@ class AuthBackendPage extends Form
|
|||
}
|
||||
|
||||
$this->info($this->translate('The configuration has been successfully validated.'));
|
||||
} elseif (isset($formData['btn_discover_domain'])) {
|
||||
return parent::isValidPartial($formData);
|
||||
} elseif (! isset($formData['backend_validation'])) {
|
||||
// This is usually done by isValid(Partial), but as we're not calling any of these...
|
||||
$this->populate($formData);
|
||||
|
|
Loading…
Reference in New Issue