summaryrefslogtreecommitdiffstats
path: root/application/forms/Config
diff options
context:
space:
mode:
Diffstat (limited to 'application/forms/Config')
-rw-r--r--application/forms/Config/General/ApplicationConfigForm.php93
-rw-r--r--application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php46
-rw-r--r--application/forms/Config/General/LoggingConfigForm.php142
-rw-r--r--application/forms/Config/General/ThemingConfigForm.php78
-rw-r--r--application/forms/Config/GeneralConfigForm.php40
-rw-r--r--application/forms/Config/Resource/DbResourceForm.php238
-rw-r--r--application/forms/Config/Resource/FileResourceForm.php67
-rw-r--r--application/forms/Config/Resource/LdapResourceForm.php129
-rw-r--r--application/forms/Config/Resource/SshResourceForm.php148
-rw-r--r--application/forms/Config/ResourceConfigForm.php441
-rw-r--r--application/forms/Config/User/CreateMembershipForm.php191
-rw-r--r--application/forms/Config/User/UserForm.php210
-rw-r--r--application/forms/Config/UserBackend/DbBackendForm.php82
-rw-r--r--application/forms/Config/UserBackend/ExternalBackendForm.php83
-rw-r--r--application/forms/Config/UserBackend/LdapBackendForm.php414
-rw-r--r--application/forms/Config/UserBackendConfigForm.php482
-rw-r--r--application/forms/Config/UserBackendReorderForm.php86
-rw-r--r--application/forms/Config/UserGroup/AddMemberForm.php182
-rw-r--r--application/forms/Config/UserGroup/DbUserGroupBackendForm.php79
-rw-r--r--application/forms/Config/UserGroup/LdapUserGroupBackendForm.php370
-rw-r--r--application/forms/Config/UserGroup/UserGroupBackendForm.php314
-rw-r--r--application/forms/Config/UserGroup/UserGroupForm.php158
22 files changed, 4073 insertions, 0 deletions
diff --git a/application/forms/Config/General/ApplicationConfigForm.php b/application/forms/Config/General/ApplicationConfigForm.php
new file mode 100644
index 0000000..0e5c700
--- /dev/null
+++ b/application/forms/Config/General/ApplicationConfigForm.php
@@ -0,0 +1,93 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\ResourceFactory;
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for general application options
+ *
+ * This form is not used directly but as subform to the {@link GeneralConfigForm}.
+ */
+class ApplicationConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_application');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'global_show_stacktraces',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Stacktraces'),
+ 'description' => $this->translate(
+ 'Set whether to show an exception\'s stacktrace by default. This can also'
+ . ' be set in a user\'s preferences with the appropriate permission.'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'global_show_application_state_messages',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Show Application State Messages'),
+ 'description' => $this->translate(
+ "Set whether to show application state messages."
+ . " This can also be set in a user's preferences."
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'global_module_path',
+ array(
+ 'label' => $this->translate('Module Path'),
+ 'required' => true,
+ 'value' => implode(':', Icinga::app()->getModuleManager()->getModuleDirs()),
+ 'description' => $this->translate(
+ 'Contains the directories that will be searched for available modules, separated by '
+ . 'colons. Modules that don\'t exist in these directories can still be symlinked in '
+ . 'the module folder, but won\'t show up in the list of disabled modules.'
+ )
+ )
+ );
+
+ $backends = array_keys(ResourceFactory::getResourceConfigs()->toArray());
+ $backends = array_combine($backends, $backends);
+
+ $this->addElement(
+ 'select',
+ 'global_config_resource',
+ array(
+ 'required' => true,
+ 'multiOptions' => array_merge(
+ ['' => sprintf(' - %s - ', $this->translate('Please choose'))],
+ $backends
+ ),
+ 'disable' => [''],
+ 'value' => '',
+ 'label' => $this->translate('Configuration Database')
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
new file mode 100644
index 0000000..0ff6c32
--- /dev/null
+++ b/application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php
@@ -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;
+ }
+}
diff --git a/application/forms/Config/General/LoggingConfigForm.php b/application/forms/Config/General/LoggingConfigForm.php
new file mode 100644
index 0000000..bbc7723
--- /dev/null
+++ b/application/forms/Config/General/LoggingConfigForm.php
@@ -0,0 +1,142 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Logger;
+use Icinga\Application\Logger\Writer\SyslogWriter;
+use Icinga\Application\Platform;
+use Icinga\Web\Form;
+
+/**
+ * Configuration form for logging options
+ *
+ * This form is not used directly but as subform for the {@link GeneralConfigForm}.
+ */
+class LoggingConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_logging');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $defaultType = getenv('ICINGAWEB_OFFICIAL_DOCKER_IMAGE') ? 'php' : 'syslog';
+
+ $this->addElement(
+ 'select',
+ 'logging_log',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Logging Type'),
+ 'description' => $this->translate('The type of logging to utilize.'),
+ 'value' => $defaultType,
+ 'multiOptions' => array(
+ 'syslog' => 'Syslog',
+ 'php' => $this->translate('Webserver Log', 'app.config.logging.type'),
+ 'file' => $this->translate('File', 'app.config.logging.type'),
+ 'none' => $this->translate('None', 'app.config.logging.type')
+ )
+ )
+ );
+
+ if (! isset($formData['logging_log']) || $formData['logging_log'] !== 'none') {
+ $this->addElement(
+ 'select',
+ 'logging_level',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Logging Level'),
+ 'description' => $this->translate('The maximum logging level to emit.'),
+ 'multiOptions' => array(
+ Logger::$levels[Logger::ERROR] => $this->translate('Error', 'app.config.logging.level'),
+ Logger::$levels[Logger::WARNING] => $this->translate('Warning', 'app.config.logging.level'),
+ Logger::$levels[Logger::INFO] => $this->translate('Information', 'app.config.logging.level'),
+ Logger::$levels[Logger::DEBUG] => $this->translate('Debug', 'app.config.logging.level')
+ )
+ )
+ );
+ }
+
+ if (! isset($formData['logging_log']) || in_array($formData['logging_log'], array('syslog', 'php'))) {
+ $this->addElement(
+ 'text',
+ 'logging_application',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Application Prefix'),
+ 'description' => $this->translate(
+ 'The name of the application by which to prefix log messages.'
+ ),
+ 'requirement' => $this->translate('The application prefix must not contain whitespace.'),
+ 'value' => 'icingaweb2',
+ 'validators' => array(
+ array(
+ 'Regex',
+ false,
+ array(
+ 'pattern' => '/^\S+$/',
+ 'messages' => array(
+ 'regexNotMatch' => $this->translate(
+ 'The application prefix must not contain whitespace.'
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+ if ((isset($formData['logging_log']) ? $formData['logging_log'] : $defaultType) === 'syslog') {
+ if (Platform::isWindows()) {
+ /* @see https://secure.php.net/manual/en/function.openlog.php */
+ $this->addElement(
+ 'hidden',
+ 'logging_facility',
+ array(
+ 'value' => 'user',
+ 'disabled' => true
+ )
+ );
+ } else {
+ $facilities = array_keys(SyslogWriter::$facilities);
+ $this->addElement(
+ 'select',
+ 'logging_facility',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Facility'),
+ 'description' => $this->translate('The syslog facility to utilize.'),
+ 'value' => 'user',
+ 'multiOptions' => array_combine($facilities, $facilities)
+ )
+ );
+ }
+ }
+ } elseif (isset($formData['logging_log']) && $formData['logging_log'] === 'file') {
+ $this->addElement(
+ 'text',
+ 'logging_file',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('File path'),
+ 'description' => $this->translate('The full path to the log file to write messages to.'),
+ 'value' => '/var/log/icingaweb2/icingaweb2.log',
+ 'validators' => array('WritablePathValidator')
+ )
+ );
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/General/ThemingConfigForm.php b/application/forms/Config/General/ThemingConfigForm.php
new file mode 100644
index 0000000..54ef2b1
--- /dev/null
+++ b/application/forms/Config/General/ThemingConfigForm.php
@@ -0,0 +1,78 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\General;
+
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
+use Icinga\Web\Form;
+use Icinga\Web\StyleSheet;
+
+/**
+ * Configuration form for theming options
+ *
+ * This form is not used directly but as subform for the {@link GeneralConfigForm}.
+ */
+class ThemingConfigForm extends Form
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general_theming');
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return $this
+ */
+ public function createElements(array $formData)
+ {
+ $themes = Icinga::app()->getThemes();
+ $themes[StyleSheet::DEFAULT_THEME] .= ' (' . $this->translate('default') . ')';
+
+ $this->addElement(
+ 'select',
+ 'themes_default',
+ array(
+ 'description' => $this->translate('The default theme', 'Form element description'),
+ 'disabled' => count($themes) < 2 ? 'disabled' : null,
+ 'label' => $this->translate('Default Theme', 'Form element label'),
+ 'multiOptions' => $themes,
+ 'value' => StyleSheet::DEFAULT_THEME
+ )
+ );
+
+ $this->addElement(
+ 'checkbox',
+ 'themes_disabled',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for disallowing users to change the theme. If a default theme is set, it will be'
+ . ' used nonetheless',
+ 'Form element description'
+ ),
+ 'label' => $this->translate('Users Can\'t Change Theme', 'Form element label')
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ if ($values['themes_default'] === '' || $values['themes_default'] === StyleSheet::DEFAULT_THEME) {
+ $values['themes_default'] = null;
+ }
+ if (! $values['themes_disabled']) {
+ $values['themes_disabled'] = null;
+ }
+ return $values;
+ }
+}
diff --git a/application/forms/Config/GeneralConfigForm.php b/application/forms/Config/GeneralConfigForm.php
new file mode 100644
index 0000000..5f15512
--- /dev/null
+++ b/application/forms/Config/GeneralConfigForm.php
@@ -0,0 +1,40 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+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;
+
+/**
+ * Configuration form for application-wide options
+ */
+class GeneralConfigForm extends ConfigForm
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function init()
+ {
+ $this->setName('form_config_general');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $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));
+ }
+}
diff --git a/application/forms/Config/Resource/DbResourceForm.php b/application/forms/Config/Resource/DbResourceForm.php
new file mode 100644
index 0000000..b9979ee
--- /dev/null
+++ b/application/forms/Config/Resource/DbResourceForm.php
@@ -0,0 +1,238 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Application\Platform;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying database resources
+ */
+class DbResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_db');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $dbChoices = array();
+ if (Platform::hasMysqlSupport()) {
+ $dbChoices['mysql'] = 'MySQL';
+ }
+ if (Platform::hasPostgresqlSupport()) {
+ $dbChoices['pgsql'] = 'PostgreSQL';
+ }
+ if (Platform::hasMssqlSupport()) {
+ $dbChoices['mssql'] = 'MSSQL';
+ }
+ if (Platform::hasIbmSupport()) {
+ $dbChoices['ibm'] = 'IBM (DB2)';
+ }
+ if (Platform::hasOracleSupport()) {
+ $dbChoices['oracle'] = 'Oracle';
+ }
+ if (Platform::hasOciSupport()) {
+ $dbChoices['oci'] = 'Oracle (OCI8)';
+ }
+ if (Platform::hasSqliteSupport()) {
+ $dbChoices['sqlite'] = 'SQLite';
+ }
+
+ $offerPostgres = false;
+ $offerMysql = false;
+ $dbChoice = isset($formData['db']) ? $formData['db'] : key($dbChoices);
+ if ($dbChoice === 'pgsql') {
+ $offerPostgres = true;
+ } elseif ($dbChoice === 'mysql') {
+ $offerMysql = true;
+ }
+
+ if ($dbChoice === 'oracle') {
+ $hostIsRequired = false;
+ } else {
+ $hostIsRequired = true;
+ }
+
+ $socketInfo = '';
+ if ($offerPostgres) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify the path to the unix domain socket directory'
+ );
+ } elseif ($offerMysql) {
+ $socketInfo = $this->translate(
+ 'For using unix domain sockets, specify localhost'
+ );
+ }
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'db',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Database Type'),
+ 'description' => $this->translate('The type of SQL database'),
+ 'multiOptions' => $dbChoices
+ )
+ );
+ if ($dbChoice === 'sqlite') {
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ } else {
+ $this->addElement(
+ 'text',
+ 'host',
+ array (
+ 'required' => $hostIsRequired,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate('The hostname of the database')
+ . ($socketInfo ? '. ' . $socketInfo : ''),
+ 'value' => $hostIsRequired ? 'localhost' : ''
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'description' => $this->translate('The port to use'),
+ 'label' => $this->translate('Port'),
+ 'preserveDefault' => true,
+ 'required' => $offerPostgres,
+ 'value' => $offerPostgres ? 5432 : null
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'dbname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Name'),
+ 'description' => $this->translate('The name of the database to use')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'username',
+ array (
+ 'required' => true,
+ 'label' => $this->translate('Username'),
+ 'description' => $this->translate('The user name to use for authentication')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'renderPassword' => true,
+ 'label' => $this->translate('Password'),
+ 'description' => $this->translate('The password to use for authentication')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'charset',
+ array (
+ 'description' => $this->translate('The character set for the database'),
+ 'label' => $this->translate('Character Set')
+ )
+ );
+ $this->addElement(
+ 'checkbox',
+ 'use_ssl',
+ array(
+ 'autosubmit' => true,
+ 'label' => $this->translate('Use SSL'),
+ 'description' => $this->translate(
+ 'Whether to encrypt the connection or to authenticate using certificates'
+ )
+ )
+ );
+ if (isset($formData['use_ssl']) && $formData['use_ssl']) {
+ if (defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
+ $this->addElement(
+ 'checkbox',
+ 'ssl_do_not_verify_server_cert',
+ array(
+ 'label' => $this->translate('SSL Do Not Verify Server Certificate'),
+ 'description' => $this->translate(
+ 'Whether to disable verification of the server certificate'
+ )
+ )
+ );
+ }
+ $this->addElement(
+ 'text',
+ 'ssl_key',
+ array(
+ 'label' => $this->translate('SSL Key'),
+ 'description' => $this->translate('The client key file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cert',
+ array(
+ 'label' => $this->translate('SSL Certificate'),
+ 'description' => $this->translate('The certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_ca',
+ array(
+ 'label' => $this->translate('SSL CA'),
+ 'description' => $this->translate('The CA certificate file path')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_capath',
+ array(
+ 'label' => $this->translate('SSL CA Path'),
+ 'description' => $this->translate(
+ 'The trusted CA certificates in PEM format directory path'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'ssl_cipher',
+ array(
+ 'label' => $this->translate('SSL Cipher'),
+ 'description' => $this->translate('The list of permissible ciphers')
+ )
+ );
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/FileResourceForm.php b/application/forms/Config/Resource/FileResourceForm.php
new file mode 100644
index 0000000..b98f1b4
--- /dev/null
+++ b/application/forms/Config/Resource/FileResourceForm.php
@@ -0,0 +1,67 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Zend_Validate_Callback;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying file resources
+ */
+class FileResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_file');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filename',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Filepath'),
+ 'description' => $this->translate('The filename to fetch information from'),
+ 'validators' => array('ReadablePathValidator')
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'fields',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Pattern'),
+ 'description' => $this->translate('The pattern by which to identify columns.'),
+ 'requirement' => $this->translate('The column pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/LdapResourceForm.php b/application/forms/Config/Resource/LdapResourceForm.php
new file mode 100644
index 0000000..7ffccdc
--- /dev/null
+++ b/application/forms/Config/Resource/LdapResourceForm.php
@@ -0,0 +1,129 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Web\Form;
+use Icinga\Web\Url;
+use Icinga\Protocol\Ldap\LdapConnection;
+
+/**
+ * Form class for adding/modifying ldap resources
+ */
+class LdapResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_ldap');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createElements(array $formData)
+ {
+ $defaultPort = ! array_key_exists('encryption', $formData) || $formData['encryption'] !== LdapConnection::LDAPS
+ ? 389
+ : 636;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'hostname',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Host'),
+ 'description' => $this->translate(
+ 'The hostname or address of the LDAP server to use for authentication.'
+ . ' You can also provide multiple hosts separated by a space'
+ ),
+ 'value' => 'localhost'
+ )
+ );
+ $this->addElement(
+ 'number',
+ 'port',
+ array(
+ 'required' => true,
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Port'),
+ 'description' => $this->translate('The port of the LDAP server to use for authentication'),
+ 'value' => $defaultPort
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'encryption',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Encryption'),
+ 'description' => $this->translate(
+ 'Whether to encrypt communication. Choose STARTTLS or LDAPS for encrypted communication or'
+ . ' none for unencrypted communication'
+ ),
+ 'multiOptions' => array(
+ 'none' => $this->translate('None', 'resource.ldap.encryption'),
+ LdapConnection::STARTTLS => 'STARTTLS',
+ LdapConnection::LDAPS => 'LDAPS'
+ )
+ )
+ );
+
+ $this->addElement(
+ 'text',
+ 'root_dn',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Root DN'),
+ 'description' => $this->translate(
+ 'Only the root and its child nodes will be accessible on this resource.'
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'bind_dn',
+ array(
+ 'label' => $this->translate('Bind DN'),
+ 'description' => $this->translate(
+ 'The user dn to use for querying the ldap server. Leave the dn and password empty for attempting'
+ . ' an anonymous bind'
+ )
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'bind_pw',
+ array(
+ 'renderPassword' => true,
+ 'label' => $this->translate('Bind Password'),
+ 'description' => $this->translate('The password to use for querying the ldap server')
+ )
+ );
+
+ $this->addElement(
+ 'number',
+ 'timeout',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('Timeout'),
+ 'description' => $this->translate('Connection timeout for every LDAP connection'),
+ 'value' => 5 // see LdapConnection::__construct()
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/Resource/SshResourceForm.php b/application/forms/Config/Resource/SshResourceForm.php
new file mode 100644
index 0000000..a15dc8c
--- /dev/null
+++ b/application/forms/Config/Resource/SshResourceForm.php
@@ -0,0 +1,148 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\Resource;
+
+use Icinga\Application\Icinga;
+use Icinga\Data\ConfigObject;
+use Icinga\Forms\Config\ResourceConfigForm;
+use Icinga\Web\Form;
+use Icinga\Util\File;
+use Zend_Validate_Callback;
+
+/**
+ * Form class for adding/modifying ssh identity resources
+ */
+class SshResourceForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource_ssh');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Resource Name'),
+ 'description' => $this->translate('The unique name of this resource')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('User'),
+ 'description' => $this->translate(
+ 'User to log in as on the remote Icinga instance. Please note that key-based SSH login must be'
+ . ' possible for this user'
+ )
+ )
+ );
+
+ if ($this->getRequest()->getActionName() != 'editresource') {
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ if (substr(ltrim($value), 0, 7) === 'file://'
+ || openssl_pkey_get_private($value) === false
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('The given SSH key is invalid'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+
+ $this->addElement(
+ 'textarea',
+ 'private_key',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Private Key'),
+ 'description' => $this->translate('The private key which will be used for the SSH connections'),
+ 'class' => 'resource ssh-identity',
+ 'validators' => array($callbackValidator)
+ )
+ );
+ } else {
+ $resourceName = $formData['name'];
+ $this->addElement(
+ 'note',
+ 'private_key_note',
+ array(
+ 'escape' => false,
+ 'label' => $this->translate('Private Key'),
+ 'value' => sprintf(
+ '<a href="%1$s" data-base-target="_next" title="%2$s" aria-label="%2$s">%3$s</a>',
+ $this->getView()->url('config/removeresource', array('resource' => $resourceName)),
+ $this->getView()->escape(sprintf($this->translate(
+ 'Remove the %s resource'
+ ), $resourceName)),
+ $this->translate('To modify the private key you must recreate this resource.')
+ )
+ )
+ );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Remove the assigned key to the resource
+ *
+ * @param ConfigObject $config
+ *
+ * @return bool
+ */
+ public static function beforeRemove(ConfigObject $config)
+ {
+ $file = $config->private_key;
+
+ if (file_exists($file)) {
+ unlink($file);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Creates the assigned key to the resource
+ *
+ * @param ResourceConfigForm $form
+ *
+ * @return bool
+ */
+ public static function beforeAdd(ResourceConfigForm $form)
+ {
+ $configDir = Icinga::app()->getConfigDir();
+ $user = $form->getElement('user')->getValue();
+
+ $filePath = join(DIRECTORY_SEPARATOR, [$configDir, 'ssh', sha1($user)]);
+ if (! file_exists($filePath)) {
+ $file = File::create($filePath, 0600);
+ } else {
+ $form->error(
+ sprintf($form->translate('The private key for the user "%s" already exists.'), $user)
+ );
+ return false;
+ }
+
+ $file->fwrite($form->getElement('private_key')->getValue());
+
+ $form->getElement('private_key')->setValue($filePath);
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/ResourceConfigForm.php b/application/forms/Config/ResourceConfigForm.php
new file mode 100644
index 0000000..fe12aca
--- /dev/null
+++ b/application/forms/Config/ResourceConfigForm.php
@@ -0,0 +1,441 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Application\Config;
+use InvalidArgumentException;
+use Icinga\Application\Platform;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Data\ResourceFactory;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Config\Resource\DbResourceForm;
+use Icinga\Forms\Config\Resource\FileResourceForm;
+use Icinga\Forms\Config\Resource\LdapResourceForm;
+use Icinga\Forms\Config\Resource\SshResourceForm;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+class ResourceConfigForm extends ConfigForm
+{
+ /**
+ * Bogus password when inspecting password elements
+ *
+ * @var string
+ */
+ protected static $dummyPassword = '_web_form_5847ed1b5b8ca';
+
+ /**
+ * If the global config must be updated because a resource has been changed, this is the updated global config
+ *
+ * @var Config|null
+ */
+ protected $updatedAppConfig = null;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_resource');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ }
+
+ /**
+ * Return a form object for the given resource type
+ *
+ * @param string $type The resource type for which to return a form
+ *
+ * @return Form
+ */
+ public function getResourceForm($type)
+ {
+ if ($type === 'db') {
+ return new DbResourceForm();
+ } elseif ($type === 'ldap') {
+ return new LdapResourceForm();
+ } elseif ($type === 'file') {
+ return new FileResourceForm();
+ } elseif ($type === 'ssh') {
+ return new SshResourceForm();
+ } else {
+ throw new InvalidArgumentException(sprintf($this->translate('Invalid resource type "%s" provided'), $type));
+ }
+ }
+
+ /**
+ * Add a particular resource
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $values The values to extend the configuration with
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case the resource does already exist
+ */
+ public function add(array $values)
+ {
+ $name = isset($values['name']) ? $values['name'] : '';
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif ($this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Resource already exists'));
+ }
+
+ unset($values['name']);
+ $this->config->setSection($name, $values);
+ return $this;
+ }
+
+ /**
+ * Edit a particular resource
+ *
+ * @param string $name The name of the resource to edit
+ * @param array $values The values to edit the configuration with
+ *
+ * @return array The edited configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function edit($name, array $values)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Old resource name missing'));
+ } elseif (! ($newName = isset($values['name']) ? $values['name'] : '')) {
+ throw new InvalidArgumentException($this->translate('New resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $this->config->removeSection($name);
+ unset($values['name']);
+ $this->config->setSection($newName, $resourceConfig->merge($values));
+
+ if ($newName !== $name) {
+ $appConfig = Config::app();
+ $section = $appConfig->getSection('global');
+ if ($section->config_resource === $name) {
+ $section->config_resource = $newName;
+ $this->updatedAppConfig = $appConfig->setSection('global', $section);
+ }
+ }
+
+ return $resourceConfig;
+ }
+
+ /**
+ * Remove a particular resource
+ *
+ * @param string $name The name of the resource to remove
+ *
+ * @return array The removed resource configuration
+ *
+ * @throws InvalidArgumentException In case the resource does not exist
+ */
+ public function remove($name)
+ {
+ if (! $name) {
+ throw new InvalidArgumentException($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($name)) {
+ throw new InvalidArgumentException($this->translate('Unknown resource provided'));
+ }
+
+ $resourceConfig = $this->config->getSection($name);
+ $resourceForm = $this->getResourceForm($resourceConfig->type);
+ if (method_exists($resourceForm, 'beforeRemove')) {
+ $resourceForm::beforeRemove($resourceConfig);
+ }
+
+ $this->config->removeSection($name);
+ return $resourceConfig;
+ }
+
+ /**
+ * Add or edit a resource and save the configuration
+ *
+ * Performs a connectivity validation using the submitted values. A checkbox is
+ * added to the form to skip the check if it fails and redirection is aborted.
+ *
+ * @see Form::onSuccess()
+ */
+ public function onSuccess()
+ {
+ $resourceForm = $this->getResourceForm($this->getElement('type')->getValue());
+
+ if (($el = $this->getElement('force_creation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ $this->addElement($this->getForceCreationCheckbox());
+ return false;
+ }
+ }
+
+ $resource = $this->request->getQuery('resource');
+ try {
+ if ($resource === null) { // create new resource
+ if (method_exists($resourceForm, 'beforeAdd')) {
+ if (! $resourceForm::beforeAdd($this)) {
+ return false;
+ }
+ }
+ $this->add(static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully created');
+ } else { // edit existing resource
+ $this->edit($resource, static::transformEmptyValuesToNull($this->getValues()));
+ $message = $this->translate('Resource "%s" has been successfully changed');
+ }
+ } catch (InvalidArgumentException $e) {
+ Notification::error($e->getMessage());
+ return false;
+ }
+
+ if ($this->save()) {
+ Notification::success(sprintf($message, $this->getElement('name')->getValue()));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Populate the form in case a resource is being edited
+ *
+ * @see Form::onRequest()
+ *
+ * @throws ConfigurationError In case the backend name is missing in the request or is invalid
+ */
+ public function onRequest()
+ {
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null) {
+ if ($resource === '') {
+ throw new ConfigurationError($this->translate('Resource name missing'));
+ } elseif (! $this->config->hasSection($resource)) {
+ throw new ConfigurationError($this->translate('Unknown resource provided'));
+ }
+ $configValues = $this->config->getSection($resource)->toArray();
+ $configValues['name'] = $resource;
+ $this->populate($configValues);
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password' && strlen($element->getValue())) {
+ $element->setValue(static::$dummyPassword);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return a checkbox to be displayed at the beginning of the form
+ * which allows the user to skip the connection validation
+ *
+ * @return Zend_Form_Element
+ */
+ protected function getForceCreationCheckbox()
+ {
+ return $this->createElement(
+ 'checkbox',
+ 'force_creation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Force Changes'),
+ 'description' => $this->translate('Check this box to enforce changes without connectivity validation')
+ )
+ );
+ }
+
+ /**
+ * @see Form::createElemeents()
+ */
+ public function createElements(array $formData)
+ {
+ $resourceType = isset($formData['type']) ? $formData['type'] : 'db';
+
+ $resourceTypes = array(
+ 'file' => $this->translate('File'),
+ 'ssh' => $this->translate('SSH Identity'),
+ );
+ if ($resourceType === 'ldap' || Platform::hasLdapSupport()) {
+ $resourceTypes['ldap'] = 'LDAP';
+ }
+ if ($resourceType === 'db' || Platform::hasDatabaseSupport()) {
+ $resourceTypes['db'] = $this->translate('SQL Database');
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Resource Type'),
+ 'description' => $this->translate('The type of resource'),
+ 'multiOptions' => $resourceTypes,
+ 'value' => $resourceType
+ )
+ );
+
+ if (isset($formData['force_creation']) && $formData['force_creation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addElement($this->getForceCreationCheckbox());
+ }
+
+ $this->addElements($this->getResourceForm($resourceType)->createElements($formData)->getElements());
+ }
+
+ /**
+ * Create a resource by using the given form's values and return its inspection results
+ *
+ * @param Form $form
+ *
+ * @return Inspection
+ */
+ public static function inspectResource(Form $form)
+ {
+ if ($form->getValue('type') !== 'ssh') {
+ $resource = ResourceFactory::createResource(new ConfigObject($form->getValues()));
+ if ($resource instanceof Inspectable) {
+ return $resource->inspect();
+ }
+ }
+ }
+
+ /**
+ * Run the configured resource's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'resource_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if ($this->getElement('resource_validation')->isChecked() && parent::isValid($formData)) {
+ $inspection = static::inspectResource($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'resource_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+
+ $this->setAttrib('data-progress-element', 'resource-progress');
+ $this->addElement(
+ 'note',
+ 'resource-progress',
+ array(
+ 'decorators' => array(
+ 'ViewHelper',
+ array('Spinner', array('id' => 'resource-progress'))
+ )
+ )
+ );
+
+ $this->addDisplayGroup(
+ array('btn_submit', 'resource_validation', 'resource-progress'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ $resource = $this->request->getQuery('resource');
+ if ($resource !== null && $this->config->hasSection($resource)) {
+ $resourceConfig = $this->config->getSection($resource)->toArray();
+ foreach ($this->getElements() as $element) {
+ if ($element->getType() === 'Zend_Form_Element_Password') {
+ $name = $element->getName();
+ if (isset($values[$name]) && $values[$name] === static::$dummyPassword) {
+ if (isset($resourceConfig[$name])) {
+ $values[$name] = $resourceConfig[$name];
+ } else {
+ unset($values[$name]);
+ }
+ }
+ }
+ }
+ }
+
+ return $values;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function writeConfig(Config $config)
+ {
+ parent::writeConfig($config);
+ if ($this->updatedAppConfig !== null) {
+ $this->updatedAppConfig->saveIni();
+ }
+ }
+}
diff --git a/application/forms/Config/User/CreateMembershipForm.php b/application/forms/Config/User/CreateMembershipForm.php
new file mode 100644
index 0000000..2828e95
--- /dev/null
+++ b/application/forms/Config/User/CreateMembershipForm.php
@@ -0,0 +1,191 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\User;
+
+use Exception;
+use Icinga\Application\Logger;
+use Icinga\Data\DataArray\ArrayDatasource;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for creating one or more group memberships
+ */
+class CreateMembershipForm extends Form
+{
+ /**
+ * The user group backends to fetch groups from
+ *
+ * Each backend must implement the Icinga\Data\Extensible and Icinga\Data\Selectable interface.
+ *
+ * @var array
+ */
+ protected $backends;
+
+ /**
+ * The username to create memberships for
+ *
+ * @var string
+ */
+ protected $userName;
+
+ /**
+ * Set the user group backends to fetch groups from
+ *
+ * @param array $backends
+ *
+ * @return $this
+ */
+ public function setBackends($backends)
+ {
+ $this->backends = $backends;
+ return $this;
+ }
+
+ /**
+ * Set the username to create memberships for
+ *
+ * @param string $userName
+ *
+ * @return $this
+ */
+ public function setUsername($userName)
+ {
+ $this->userName = $userName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ $query = $this->createDataSource()->select()->from('group', array('group_name', 'backend_name'));
+
+ $options = array();
+ foreach ($query as $row) {
+ $options[$row->backend_name . ';' . $row->group_name] = $row->group_name . ' (' . $row->backend_name . ')';
+ }
+
+ $this->addElement(
+ 'multiselect',
+ 'groups',
+ array(
+ 'required' => true,
+ 'multiOptions' => $options,
+ 'label' => $this->translate('Groups'),
+ 'description' => sprintf(
+ $this->translate('Select one or more groups where to add %s as member'),
+ $this->userName
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Create memberships for %s'), $this->userName));
+ $this->setSubmitLabel($this->translate('Create'));
+ }
+
+ /**
+ * Instantly redirect back in case the user is already a member of all groups
+ */
+ public function onRequest()
+ {
+ if ($this->createDataSource()->select()->from('group')->count() === 0) {
+ Notification::info(sprintf($this->translate('User %s is already a member of all groups'), $this->userName));
+ $this->getResponse()->redirectAndExit($this->getRedirectUrl());
+ }
+ }
+
+ /**
+ * Create the memberships for the user
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $backendMap = array();
+ foreach ($this->backends as $backend) {
+ $backendMap[$backend->getName()] = $backend;
+ }
+
+ $single = null;
+ foreach ($this->getValue('groups') as $backendAndGroup) {
+ list($backendName, $groupName) = explode(';', $backendAndGroup, 2);
+ try {
+ $backendMap[$backendName]->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $groupName,
+ 'user_name' => $this->userName
+ )
+ );
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $this->userName,
+ $groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(
+ sprintf($this->translate('Membership for group %s created successfully'), $groupName)
+ );
+ } else {
+ Notification::success($this->translate('Memberships created successfully'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Create and return a data source to fetch all groups from all backends where the user is not already a member of
+ *
+ * @return ArrayDatasource
+ */
+ protected function createDataSource()
+ {
+ $groups = $failures = array();
+ foreach ($this->backends as $backend) {
+ try {
+ $memberships = $backend
+ ->select()
+ ->from('group_membership', array('group_name'))
+ ->where('user_name', $this->userName)
+ ->fetchColumn();
+ foreach ($backend->select(array('group_name')) as $row) {
+ if (! in_array($row->group_name, $memberships)) { // TODO(jom): Apply this as native query filter
+ $row->backend_name = $backend->getName();
+ $groups[] = $row;
+ }
+ }
+ } catch (Exception $e) {
+ $failures[] = array($backend->getName(), $e);
+ }
+ }
+
+ if (empty($groups) && !empty($failures)) {
+ // In case there are only failures, throw the very first exception again
+ throw $failures[0][1];
+ } elseif (! empty($failures)) {
+ foreach ($failures as $failure) {
+ Logger::error($failure[1]);
+ Notification::warning(sprintf(
+ $this->translate('Failed to fetch any groups from backend %s. Please check your log'),
+ $failure[0]
+ ));
+ }
+ }
+
+ return new ArrayDatasource($groups);
+ }
+}
diff --git a/application/forms/Config/User/UserForm.php b/application/forms/Config/User/UserForm.php
new file mode 100644
index 0000000..fb2ef4d
--- /dev/null
+++ b/application/forms/Config/User/UserForm.php
@@ -0,0 +1,210 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\User;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Web\Notification;
+
+class UserForm extends RepositoryForm
+{
+ /**
+ * Create and add elements to this form to insert or update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'checkbox',
+ 'is_active',
+ array(
+ 'value' => true,
+ 'label' => $this->translate('Active'),
+ 'description' => $this->translate('Prevents the user from logging in if unchecked')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Username')
+ )
+ );
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Password')
+ )
+ );
+
+ $this->setTitle($this->translate('Add a new user'));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Create and add elements to this form to update a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createUpdateElements(array $formData)
+ {
+ $this->createInsertElements($formData);
+
+ $this->addElement(
+ 'password',
+ 'password',
+ array(
+ 'description' => $this->translate('Leave empty for not updating the user\'s password'),
+ 'label' => $this->translate('Password'),
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Edit user %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+
+ /**
+ * Update a user
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('user_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('user', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Retrieve all form element values
+ *
+ * Strips off the password if null or the empty string.
+ *
+ * @param bool $suppressArrayNotation
+ *
+ * @return array
+ */
+ public function getValues($suppressArrayNotation = false)
+ {
+ $values = parent::getValues($suppressArrayNotation);
+ // before checking if password values is empty
+ // we have to check that the password field is set
+ // otherwise an error is thrown
+ if (isset($values['password']) && ! $values['password']) {
+ unset($values['password']);
+ }
+
+ return $values;
+ }
+
+ /**
+ * Create and add elements to this form to delete a user
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove user %s?'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a user
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('user_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('User added successfully');
+ } else {
+ return $this->translate('Failed to add user');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a user
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('User "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove user "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}
diff --git a/application/forms/Config/UserBackend/DbBackendForm.php b/application/forms/Config/UserBackend/DbBackendForm.php
new file mode 100644
index 0000000..693ea14
--- /dev/null
+++ b/application/forms/Config/UserBackend/DbBackendForm.php
@@ -0,0 +1,82 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying database user backends
+ */
+class DbBackendForm extends Form
+{
+ /**
+ * The database resource names the user can choose from
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_db');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate(
+ 'The database connection to use for authenticating with this provider'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array()
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'db'
+ )
+ );
+ }
+}
diff --git a/application/forms/Config/UserBackend/ExternalBackendForm.php b/application/forms/Config/UserBackend/ExternalBackendForm.php
new file mode 100644
index 0000000..f4a4639
--- /dev/null
+++ b/application/forms/Config/UserBackend/ExternalBackendForm.php
@@ -0,0 +1,83 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserBackend;
+
+use Zend_Validate_Callback;
+use Icinga\Web\Form;
+
+/**
+ * Form class for adding/modifying user backends of type "external"
+ */
+class ExternalBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_external');
+ }
+
+ /**
+ * @see Form::createElements()
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others'
+ )
+ )
+ );
+ $callbackValidator = new Zend_Validate_Callback(function ($value) {
+ return @preg_match($value, '') !== false;
+ });
+ $callbackValidator->setMessage(
+ $this->translate('"%value%" is not a valid regular expression.'),
+ Zend_Validate_Callback::INVALID_VALUE
+ );
+ $this->addElement(
+ 'text',
+ 'strip_username_regexp',
+ array(
+ 'label' => $this->translate('Filter Pattern'),
+ 'description' => $this->translate(
+ 'The filter to use to strip specific parts off from usernames.'
+ . ' Leave empty if you do not want to strip off anything.'
+ ),
+ 'requirement' => $this->translate('The filter pattern must be a valid regular expression.'),
+ 'validators' => array($callbackValidator)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => 'external'
+ )
+ );
+
+ return $this;
+ }
+
+ /**
+ * Validate the configuration by creating a backend and requesting the user count
+ *
+ * Returns always true as backends of type "external" are just "passive" backends.
+ *
+ * @param Form $form The form to fetch the configuration values from
+ *
+ * @return bool Whether validation succeeded or not
+ */
+ public static function isValidUserBackend(Form $form)
+ {
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserBackend/LdapBackendForm.php b/application/forms/Config/UserBackend/LdapBackendForm.php
new file mode 100644
index 0000000..e7804cc
--- /dev/null
+++ b/application/forms/Config/UserBackend/LdapBackendForm.php
@@ -0,0 +1,414 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+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;
+
+/**
+ * Form class for adding/modifying LDAP user backends
+ */
+class LdapBackendForm extends Form
+{
+ /**
+ * The ldap resource names the user can choose from
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * Default values for the form elements
+ *
+ * @var string[]
+ */
+ protected $suggestions = array();
+
+ /**
+ * Cache for {@link getLdapCapabilities()}
+ *
+ * @var LdapCapabilities
+ */
+ protected $ldapCapabilities;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend_ldap');
+ }
+
+ /**
+ * Set the resource names the user can choose from
+ *
+ * @param array $resources The resources to choose from
+ *
+ * @return $this
+ */
+ public function setResources(array $resources)
+ {
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $isAd = isset($formData['type']) ? $formData['type'] === 'msldap' : false;
+
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this authentication provider that is used to differentiate it from others.'
+ ),
+ 'value' => $this->getSuggestion('name')
+ )
+ );
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate(
+ 'The LDAP connection to use for authenticating with this provider.'
+ ),
+ 'multiOptions' => !empty($this->resources)
+ ? array_combine($this->resources, $this->resources)
+ : array(),
+ 'value' => $this->getSuggestion('resource')
+ )
+ );
+
+ if (! $isAd && !empty($this->resources)) {
+ $this->addElement(
+ 'button',
+ 'discovery_btn',
+ array(
+ 'class' => 'control-button',
+ 'type' => 'submit',
+ 'value' => 'discovery_btn',
+ 'label' => $this->translate('Discover', 'A button to discover LDAP capabilities'),
+ 'title' => $this->translate(
+ 'Push to fill in the chosen connection\'s default settings.'
+ ),
+ 'decorators' => array(
+ array('ViewHelper', array('separator' => '')),
+ array('Spinner'),
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ ),
+ 'formnovalidate' => 'formnovalidate'
+ )
+ );
+ }
+
+ if ($isAd) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $this->getSuggestion('user_class', $userClass)
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'value' => $this->getSuggestion('filter', $filter),
+ 'label' => $this->translate('LDAP Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression.'
+ . ' (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ // This is not meant to be a full syntax check. It will just
+ // ensure that we can safely strip unnecessary parentheses.
+ $v = trim($v);
+ return ! $v || $v[0] !== '(' || (
+ strpos($v, ')(') !== false ? substr($v, -2) === '))' : substr($v, -1) === ')'
+ );
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter is invalid. Please check your syntax.')
+ )
+ )
+ )
+ )
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'required' => ! $isAd,
+ 'ignore' => $isAd,
+ 'disabled' => $isAd ?: null,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing the user name on the LDAP server.'
+ ),
+ 'value' => $this->getSuggestion('user_name_attribute', $userNameAttribute)
+ )
+ );
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true,
+ 'value' => $this->getSuggestion('backend', $isAd ? 'msldap' : 'ldap')
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'required' => false,
+ 'label' => $this->translate('LDAP Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $this->getSuggestion('base_dn')
+ )
+ );
+
+ $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.'
+ ),
+ 'preserveDefault' => true,
+ 'value' => $this->getSuggestion('domain')
+ )
+ );
+
+ $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'
+ )
+ );
+ }
+
+ public function isValidPartial(array $formData)
+ {
+ $isAd = isset($formData['type']) && $formData['type'] === 'msldap';
+ $baseDn = null;
+ $hasAdOid = false;
+ $discoverySuccessful = false;
+
+ if (! $isAd && ! empty($this->resources) && isset($formData['discovery_btn'])
+ && $formData['discovery_btn'] === 'discovery_btn') {
+ $discoverySuccessful = true;
+ try {
+ $capabilities = $this->getLdapCapabilities($formData);
+ $baseDn = $capabilities->getDefaultNamingContext();
+ $hasAdOid = $capabilities->isActiveDirectory();
+ } catch (Exception $e) {
+ $this->warning(sprintf(
+ $this->translate('Failed to discover the chosen LDAP connection: %s'),
+ $e->getMessage()
+ ));
+ $discoverySuccessful = false;
+ }
+ }
+
+ if ($discoverySuccessful) {
+ if ($isAd || $hasAdOid) {
+ // ActiveDirectory defaults
+ $userClass = 'user';
+ $filter = '!(objectClass=computer)';
+ $userNameAttribute = 'sAMAccountName';
+ } else {
+ // OpenLDAP defaults
+ $userClass = 'inetOrgPerson';
+ $filter = null;
+ $userNameAttribute = 'uid';
+ }
+
+ $formData['user_class'] = $userClass;
+
+ if (! isset($formData['filter']) || $formData['filter'] === '') {
+ $formData['filter'] = $filter;
+ }
+
+ $formData['user_name_attribute'] = $userNameAttribute;
+
+ if ($baseDn !== null && (! isset($formData['base_dn']) || $formData['base_dn'] === '')) {
+ $formData['base_dn'] = $baseDn;
+ }
+ }
+
+ if (isset($formData['btn_discover_domain']) && $formData['btn_discover_domain'] === 'discovery_btn') {
+ try {
+ $formData['domain'] = $this->discoverDomain($formData);
+ } catch (LdapException $e) {
+ $this->error($e->getMessage());
+ }
+ }
+
+ return parent::isValidPartial($formData);
+ }
+
+ /**
+ * Get the LDAP capabilities of either the resource specified by the user or the default one
+ *
+ * @param string[] $formData
+ *
+ * @return LdapCapabilities
+ */
+ protected function getLdapCapabilities(array $formData)
+ {
+ if ($this->ldapCapabilities === null) {
+ $this->ldapCapabilities = ResourceFactory::create(
+ isset($formData['resource']) ? $formData['resource'] : reset($this->resources)
+ )->bind()->getCapabilities();
+ }
+
+ return $this->ldapCapabilities;
+ }
+
+ /**
+ * Discover the domain the LDAP server is responsible for
+ *
+ * @param string[] $formData
+ *
+ * @return string
+ */
+ protected function discoverDomain(array $formData)
+ {
+ $cap = $this->getLdapCapabilities($formData);
+
+ 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]);
+ }
+ }
+ }
+
+ /**
+ * Get the default values for the form elements
+ *
+ * @return string[]
+ */
+ public function getSuggestions()
+ {
+ return $this->suggestions;
+ }
+
+ /**
+ * Get the default value for the given form element or the given default
+ *
+ * @param string $element
+ * @param string $default
+ *
+ * @return string
+ */
+ public function getSuggestion($element, $default = null)
+ {
+ return isset($this->suggestions[$element]) ? $this->suggestions[$element] : $default;
+ }
+
+ /**
+ * Set the default values for the form elements
+ *
+ * @param string[] $suggestions
+ *
+ * @return $this
+ */
+ public function setSuggestions(array $suggestions)
+ {
+ $this->suggestions = $suggestions;
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendConfigForm.php b/application/forms/Config/UserBackendConfigForm.php
new file mode 100644
index 0000000..fdca657
--- /dev/null
+++ b/application/forms/Config/UserBackendConfigForm.php
@@ -0,0 +1,482 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use InvalidArgumentException;
+use Icinga\Application\Config;
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Forms\ConfigForm;
+use Icinga\Forms\Config\UserBackend\ExternalBackendForm;
+use Icinga\Forms\Config\UserBackend\DbBackendForm;
+use Icinga\Forms\Config\UserBackend\LdapBackendForm;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing user backends
+ */
+class UserBackendConfigForm extends ConfigForm
+{
+ /**
+ * The available user backend resources split by type
+ *
+ * @var array
+ */
+ protected $resources;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * The loaded custom backends list
+ *
+ * @var array
+ */
+ protected $customBackends;
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_authbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->setValidatePartial(true);
+ $this->customBackends = UserBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Set the resource configuration to use
+ *
+ * @param Config $resourceConfig The resource configuration
+ *
+ * @return $this
+ *
+ * @throws ConfigurationError In case there are no valid resources for authentication available
+ */
+ public function setResourceConfig(Config $resourceConfig)
+ {
+ $resources = array();
+ foreach ($resourceConfig as $name => $resource) {
+ if (in_array($resource->type, array('db', 'ldap'))) {
+ $resources[$resource->type][] = $name;
+ }
+ }
+
+ if (empty($resources)) {
+ $externalBackends = $this->config->toArray();
+ array_walk(
+ $externalBackends,
+ function (&$authBackendCfg) {
+ if (! isset($authBackendCfg['backend']) || $authBackendCfg['backend'] !== 'external') {
+ $authBackendCfg = null;
+ }
+ }
+ );
+ if (count(array_filter($externalBackends)) > 0 && (
+ $this->backendToLoad === null || !isset($externalBackends[$this->backendToLoad])
+ )) {
+ throw new ConfigurationError($this->translate(
+ 'Could not find any valid user backend resources.'
+ . ' Please configure a resource for authentication first.'
+ ));
+ }
+ }
+
+ $this->resources = $resources;
+ return $this;
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ $form = new DbBackendForm();
+ $form->setResources(isset($this->resources['db']) ? $this->resources['db'] : array());
+ break;
+ case 'ldap':
+ case 'msldap':
+ $form = new LdapBackendForm();
+ $form->setResources(isset($this->resources['ldap']) ? $this->resources['ldap'] : array());
+ break;
+ case 'external':
+ $form = new ExternalBackendForm();
+ break;
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+
+ return $form;
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException(
+ $this->translate('A user backend with the name "%s" does already exist'),
+ $backendName
+ );
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $backendConfig->merge($data);
+ $this->config->setSection($name, $backendConfig);
+ return $this;
+ }
+
+ /**
+ * Remove a user backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Move the given user backend up or down in order
+ *
+ * @param string $name The name of the backend to be moved
+ * @param int $position The new (absolute) position of the backend
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function move($name, $position)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user backend called "%s" found', $name);
+ }
+
+ $backendOrder = $this->config->keys();
+ array_splice($backendOrder, array_search($name, $backendOrder), 1);
+ array_splice($backendOrder, $position, 0, $name);
+
+ $newConfig = array();
+ foreach ($backendOrder as $backendName) {
+ $newConfig[$backendName] = $this->config->getSection($backendName);
+ }
+
+ $config = Config::fromArray($newConfig);
+ $this->config = $config->setConfigFile($this->config->getConfigFile());
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array();
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+
+ if (isset($this->resources['db'])) {
+ $backendTypes['db'] = $this->translate('Database');
+ }
+ if (isset($this->resources['ldap'])) {
+ $backendTypes['ldap'] = 'LDAP';
+ $backendTypes['msldap'] = 'ActiveDirectory';
+ }
+
+ $externalBackends = array_filter(
+ $this->config->toArray(),
+ function ($authBackendCfg) {
+ return isset($authBackendCfg['backend']) && $authBackendCfg['backend'] === 'external';
+ }
+ );
+ if ($backendType === 'external' || empty($externalBackends)) {
+ $backendTypes['external'] = $this->translate('External');
+ }
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate(
+ 'The type of the resource to use for this authenticaton provider'
+ ),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ if (isset($formData['skip_validation']) && $formData['skip_validation']) {
+ // In case another error occured and the checkbox was displayed before
+ $this->addSkipValidationCheckbox();
+ }
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['name'] = $this->backendToLoad;
+ $data['type'] = $data['backend'];
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Return whether the given values are valid
+ *
+ * @param array $formData The data to validate
+ *
+ * @return bool
+ */
+ public function isValid($formData)
+ {
+ if (! parent::isValid($formData)) {
+ return false;
+ }
+
+ if (($el = $this->getElement('skip_validation')) === null || false === $el->isChecked()) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection && $inspection->hasError()) {
+ $this->error($inspection->getError());
+ if ($el === null) {
+ $this->addSkipValidationCheckbox();
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Create a user backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Add a checkbox to the form by which the user can skip the connection validation
+ */
+ protected function addSkipValidationCheckbox()
+ {
+ $this->addElement(
+ 'checkbox',
+ 'skip_validation',
+ array(
+ 'order' => 0,
+ 'ignore' => true,
+ 'label' => $this->translate('Skip Validation'),
+ 'description' => $this->translate(
+ 'Check this box to enforce changes without validating that authentication is possible.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ 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) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserBackendReorderForm.php b/application/forms/Config/UserBackendReorderForm.php
new file mode 100644
index 0000000..019c032
--- /dev/null
+++ b/application/forms/Config/UserBackendReorderForm.php
@@ -0,0 +1,86 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config;
+
+use Icinga\Application\Config;
+use Icinga\Forms\ConfigForm;
+use Icinga\Exception\NotFoundError;
+use Icinga\Web\Notification;
+
+class UserBackendReorderForm extends ConfigForm
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_reorder_authbackend');
+ $this->setViewScript('form/reorder-authbackend.phtml');
+ }
+
+ /**
+ * Return the ordered backend names
+ *
+ * @return array
+ */
+ public function getBackendOrder()
+ {
+ return $this->config->keys();
+ }
+
+ /**
+ * Return the ordered backend configuration
+ *
+ * @return Config
+ */
+ public function getConfig()
+ {
+ return $this->config;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ // This adds just a dummy element to be able to utilize Form::getValue as part of onSuccess()
+ $this->addElement('hidden', 'backend_newpos');
+ }
+
+ /**
+ * Update the user backend order and save the configuration
+ */
+ public function onSuccess()
+ {
+ $newPosData = $this->getValue('backend_newpos');
+ if ($newPosData) {
+ $configForm = $this->getConfigForm();
+ list($backendName, $position) = explode('|', $newPosData, 2);
+
+ try {
+ if ($configForm->move($backendName, $position)->save()) {
+ Notification::success($this->translate('Authentication order updated'));
+ } else {
+ return false;
+ }
+ } catch (NotFoundError $_) {
+ Notification::error(sprintf($this->translate('User backend "%s" not found'), $backendName));
+ }
+ }
+ }
+
+ /**
+ * Return the config form for user backends
+ *
+ * @return ConfigForm
+ */
+ protected function getConfigForm()
+ {
+ $form = new UserBackendConfigForm();
+ $form->setIniConfig($this->config);
+ return $form;
+ }
+}
diff --git a/application/forms/Config/UserGroup/AddMemberForm.php b/application/forms/Config/UserGroup/AddMemberForm.php
new file mode 100644
index 0000000..debb9b7
--- /dev/null
+++ b/application/forms/Config/UserGroup/AddMemberForm.php
@@ -0,0 +1,182 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Exception;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Selectable;
+use Icinga\Exception\NotFoundError;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for adding one or more group members
+ */
+class AddMemberForm extends Form
+{
+ /**
+ * The data source to fetch users from
+ *
+ * @var Selectable
+ */
+ protected $ds;
+
+ /**
+ * The user group backend to use
+ *
+ * @var Extensible
+ */
+ protected $backend;
+
+ /**
+ * The group to add members for
+ *
+ * @var string
+ */
+ protected $groupName;
+
+ /**
+ * Set the data source to fetch users from
+ *
+ * @param Selectable $ds
+ *
+ * @return $this
+ */
+ public function setDataSource(Selectable $ds)
+ {
+ $this->ds = $ds;
+ return $this;
+ }
+
+ /**
+ * Set the user group backend to use
+ *
+ * @param Extensible $backend
+ *
+ * @return $this
+ */
+ public function setBackend(Extensible $backend)
+ {
+ $this->backend = $backend;
+ return $this;
+ }
+
+ /**
+ * Set the group to add members for
+ *
+ * @param string $groupName
+ *
+ * @return $this
+ */
+ public function setGroupName($groupName)
+ {
+ $this->groupName = $groupName;
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData The data sent by the user
+ */
+ public function createElements(array $formData)
+ {
+ // TODO(jom): Fetching already existing members to prevent the user from mistakenly creating duplicate
+ // memberships (no matter whether the data source permits it or not, a member does never need to be
+ // added more than once) should be kept at backend level (GroupController::fetchUsers) but this does
+ // not work currently as our ldap protocol stuff is unable to handle our filter implementation..
+ $members = $this->backend
+ ->select()
+ ->from('group_membership', array('user_name'))
+ ->where('group_name', $this->groupName)
+ ->fetchColumn();
+ $filter = empty($members) ? Filter::matchAll() : Filter::not(Filter::where('user_name', $members));
+
+ $users = $this->ds->select()->from('user', array('user_name'))->applyFilter($filter)->fetchColumn();
+ if (! empty($users)) {
+ $this->addElement(
+ 'multiselect',
+ 'user_name',
+ array(
+ 'multiOptions' => array_combine($users, $users),
+ 'label' => $this->translate('Backend Users'),
+ 'description' => $this->translate(
+ 'Select one or more users (fetched from your user backends) to add as group member'
+ ),
+ 'class' => 'grant-permissions'
+ )
+ );
+ }
+
+ $this->addElement(
+ 'textarea',
+ 'users',
+ array(
+ 'required' => empty($users),
+ 'label' => $this->translate('Users'),
+ 'description' => $this->translate(
+ 'Provide one or more usernames separated by comma to add as group member'
+ )
+ )
+ );
+
+ $this->setTitle(sprintf($this->translate('Add members for group %s'), $this->groupName));
+ $this->setSubmitLabel($this->translate('Add'));
+ }
+
+ /**
+ * Insert the members for the group
+ *
+ * @return bool
+ */
+ public function onSuccess()
+ {
+ $userNames = $this->getValue('user_name') ?: array();
+ if (($users = $this->getValue('users'))) {
+ $userNames = array_merge($userNames, array_map('trim', explode(',', $users)));
+ }
+
+ if (empty($userNames)) {
+ $this->info($this->translate(
+ 'Please provide at least one username, either by choosing one '
+ . 'in the list or by manually typing one in the text box below'
+ ));
+ return false;
+ }
+
+ $single = null;
+ foreach ($userNames as $userName) {
+ try {
+ $this->backend->insert(
+ 'group_membership',
+ array(
+ 'group_name' => $this->groupName,
+ 'user_name' => $userName
+ )
+ );
+ } catch (NotFoundError $e) {
+ throw $e; // Trigger 404, the group name is initially accessed as GET parameter
+ } catch (Exception $e) {
+ Notification::error(sprintf(
+ $this->translate('Failed to add "%s" as group member for "%s"'),
+ $userName,
+ $this->groupName
+ ));
+ $this->error($e->getMessage());
+ return false;
+ }
+
+ $single = $single === null;
+ }
+
+ if ($single) {
+ Notification::success(sprintf($this->translate('Group member "%s" added successfully'), $userName));
+ } else {
+ Notification::success($this->translate('Group members added successfully'));
+ }
+
+ return true;
+ }
+}
diff --git a/application/forms/Config/UserGroup/DbUserGroupBackendForm.php b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
new file mode 100644
index 0000000..daea8de
--- /dev/null
+++ b/application/forms/Config/UserGroup/DbUserGroupBackendForm.php
@@ -0,0 +1,79 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Data\ResourceFactory;
+use Icinga\Web\Form;
+
+/**
+ * Form for managing database user group backends
+ */
+class DbUserGroupBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_dbusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getDatabaseResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Database Connection'),
+ 'description' => $this->translate('The database connection to use for this backend'),
+ 'multiOptions' => empty($resourceNames) ? array() : array_combine($resourceNames, $resourceNames)
+ )
+ );
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => 'db'
+ )
+ );
+ }
+
+ /**
+ * Return the names of all configured database resources
+ *
+ * @return array
+ */
+ protected function getDatabaseResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (strtolower($config->type) === 'db') {
+ $names[] = $name;
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
new file mode 100644
index 0000000..10c069a
--- /dev/null
+++ b/application/forms/Config/UserGroup/LdapUserGroupBackendForm.php
@@ -0,0 +1,370 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Authentication\User\UserBackend;
+use Icinga\Authentication\UserGroup\LdapUserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\ResourceFactory;
+use Icinga\Protocol\Ldap\LdapConnection;
+use Icinga\Web\Form;
+use Icinga\Web\Notification;
+
+/**
+ * Form for managing LDAP user group backends
+ */
+class LdapUserGroupBackendForm extends Form
+{
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_ldapusergroupbackend');
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Backend Name'),
+ 'description' => $this->translate(
+ 'The name of this user group backend that is used to differentiate it from others'
+ )
+ )
+ );
+
+ $resourceNames = $this->getLdapResourceNames();
+ $this->addElement(
+ 'select',
+ 'resource',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('LDAP Connection'),
+ 'description' => $this->translate('The LDAP connection to use for this backend.'),
+ 'multiOptions' => array_combine($resourceNames, $resourceNames)
+ )
+ );
+ $resource = ResourceFactory::create(
+ isset($formData['resource']) && in_array($formData['resource'], $resourceNames)
+ ? $formData['resource']
+ : $resourceNames[0]
+ );
+
+ $userBackendNames = $this->getLdapUserBackendNames($resource);
+ if (! empty($userBackendNames)) {
+ $userBackends = array_combine($userBackendNames, $userBackendNames);
+ $userBackends['none'] = $this->translate('None', 'usergroupbackend.ldap.user_backend');
+ } else {
+ $userBackends = array('none' => $this->translate('None', 'usergroupbackend.ldap.user_backend'));
+ }
+ $this->addElement(
+ 'select',
+ 'user_backend',
+ array(
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('User Backend'),
+ 'description' => $this->translate('The user backend to link with this user group backend.'),
+ 'multiOptions' => $userBackends
+ )
+ );
+
+ $groupBackend = new LdapUserGroupBackend($resource);
+ if ($formData['type'] === 'ldap') {
+ $defaults = $groupBackend->getOpenLdapDefaults();
+ $groupConfigDisabled = $userConfigDisabled = null; // MUST BE null, do NOT change this to false!
+ } else { // $formData['type'] === 'msldap'
+ $defaults = $groupBackend->getActiveDirectoryDefaults();
+ $groupConfigDisabled = $userConfigDisabled = true;
+ }
+
+ if ($formData['type'] === 'msldap') {
+ $this->addElement(
+ 'checkbox',
+ 'nested_group_search',
+ array(
+ 'description' => $this->translate(
+ 'Check this box for nested group search in Active Directory based on the user'
+ ),
+ 'label' => $this->translate('Nested Group Search')
+ )
+ );
+ } else {
+ // This is required to purge already present options
+ $this->addElement('hidden', 'nested_group_search', array('disabled' => true));
+ }
+
+ $this->createGroupConfigElements($defaults, $groupConfigDisabled);
+ if (count($userBackends) === 1 || (isset($formData['user_backend']) && $formData['user_backend'] === 'none')) {
+ $this->createUserConfigElements($defaults, $userConfigDisabled);
+ } else {
+ $this->createHiddenUserConfigElements();
+ }
+
+ $this->addElement(
+ 'hidden',
+ 'backend',
+ array(
+ 'disabled' => true, // Prevents the element from being submitted, see #7717
+ 'value' => $formData['type']
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the group configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createGroupConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'group_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Object Class'),
+ 'description' => $this->translate('The object class used for storing groups on the LDAP server.'),
+ 'value' => $defaults->group_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP Group Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up groups using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->group_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a group\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->group_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'group_member_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP Group Member Attribute'),
+ 'description' => $this->translate('The attribute name used for storing a group\'s members.'),
+ 'value' => $defaults->group_member_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP Group Base DN'),
+ 'description' => $this->translate(
+ 'The path where groups can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ 'value' => $defaults->base_dn
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements to this form required for the user configuration
+ *
+ * @param ConfigObject $defaults
+ * @param null|bool $disabled
+ */
+ protected function createUserConfigElements(ConfigObject $defaults, $disabled)
+ {
+ $this->addElement(
+ 'text',
+ 'user_class',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Object Class'),
+ 'description' => $this->translate('The object class used for storing users on the LDAP server.'),
+ 'value' => $defaults->user_class
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_filter',
+ array(
+ 'preserveDefault' => true,
+ 'allowEmpty' => true,
+ 'label' => $this->translate('LDAP User Filter'),
+ 'description' => $this->translate(
+ 'An additional filter to use when looking up users using the specified connection. '
+ . 'Leave empty to not to use any additional filter rules.'
+ ),
+ 'requirement' => $this->translate(
+ 'The filter needs to be expressed as standard LDAP expression, without'
+ . ' outer parentheses. (e.g. &(foo=bar)(bar=foo) or foo=bar)'
+ ),
+ 'validators' => array(
+ array(
+ 'Callback',
+ false,
+ array(
+ 'callback' => function ($v) {
+ return strpos($v, '(') !== 0;
+ },
+ 'messages' => array(
+ 'callbackValue' => $this->translate('The filter must not be wrapped in parantheses.')
+ )
+ )
+ )
+ ),
+ 'value' => $defaults->user_filter
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_name_attribute',
+ array(
+ 'preserveDefault' => true,
+ 'ignore' => $disabled,
+ 'disabled' => $disabled,
+ 'label' => $this->translate('LDAP User Name Attribute'),
+ 'description' => $this->translate(
+ 'The attribute name used for storing a user\'s name on the LDAP server.'
+ ),
+ 'value' => $defaults->user_name_attribute
+ )
+ );
+ $this->addElement(
+ 'text',
+ 'user_base_dn',
+ array(
+ 'preserveDefault' => true,
+ 'label' => $this->translate('LDAP User Base DN'),
+ 'description' => $this->translate(
+ 'The path where users can be found on the LDAP server. Leave ' .
+ 'empty to select all users available using the specified connection.'
+ ),
+ '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.'
+ )
+ )
+ );
+ }
+
+ /**
+ * Create and add all elements for the user configuration as hidden inputs
+ *
+ * This is required to purge already present options when unlinking a group backend with a user backend.
+ */
+ protected function createHiddenUserConfigElements()
+ {
+ $this->addElement('hidden', 'user_class', array('disabled' => true));
+ $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));
+ }
+
+ /**
+ * Return the names of all configured LDAP resources
+ *
+ * @return array
+ */
+ protected function getLdapResourceNames()
+ {
+ $names = array();
+ foreach (ResourceFactory::getResourceConfigs() as $name => $config) {
+ if (in_array(strtolower($config->type), array('ldap', 'msldap'))) {
+ $names[] = $name;
+ }
+ }
+
+ if (empty($names)) {
+ Notification::error(
+ $this->translate('No LDAP resources available. Please configure an LDAP resource first.')
+ );
+ $this->getResponse()->redirectAndExit('config/createresource');
+ }
+
+ return $names;
+ }
+
+ /**
+ * Return the names of all configured LDAP user backends
+ *
+ * @param LdapConnection $resource
+ *
+ * @return array
+ */
+ protected function getLdapUserBackendNames(LdapConnection $resource)
+ {
+ $names = array();
+ foreach (UserBackend::getBackendConfigs() as $name => $config) {
+ if (in_array(strtolower($config->backend), array('ldap', 'msldap'))) {
+ $backendResource = ResourceFactory::create($config->resource);
+ if ($backendResource->getHostname() === $resource->getHostname()
+ && $backendResource->getPort() === $resource->getPort()
+ ) {
+ $names[] = $name;
+ }
+ }
+ }
+
+ return $names;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupBackendForm.php b/application/forms/Config/UserGroup/UserGroupBackendForm.php
new file mode 100644
index 0000000..9ee4032
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupBackendForm.php
@@ -0,0 +1,314 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Authentication\UserGroup\UserGroupBackend;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Inspectable;
+use Icinga\Data\Inspection;
+use Icinga\Web\Form;
+use InvalidArgumentException;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\NotFoundError;
+use Icinga\Forms\ConfigForm;
+
+/**
+ * Form for managing user group backends
+ */
+class UserGroupBackendForm extends ConfigForm
+{
+ protected $validatePartial = true;
+
+ /**
+ * The backend to load when displaying the form for the first time
+ *
+ * @var string
+ */
+ protected $backendToLoad;
+
+ /**
+ * Known custom backends
+ *
+ * @var array
+ */
+ protected $customBackends;
+
+ /**
+ * Create a user group backend by using the given form's values and return its inspection results
+ *
+ * Returns null for non-inspectable backends.
+ *
+ * @param Form $form
+ *
+ * @return Inspection|null
+ */
+ public static function inspectUserBackend(Form $form)
+ {
+ $backend = UserGroupBackend::create(null, new ConfigObject($form->getValues()));
+ if ($backend instanceof Inspectable) {
+ return $backend->inspect();
+ }
+ }
+
+ /**
+ * Initialize this form
+ */
+ public function init()
+ {
+ $this->setName('form_config_usergroupbackend');
+ $this->setSubmitLabel($this->translate('Save Changes'));
+ $this->customBackends = UserGroupBackend::getCustomBackendConfigForms();
+ }
+
+ /**
+ * Return a form object for the given backend type
+ *
+ * @param string $type The backend type for which to return a form
+ *
+ * @return Form
+ *
+ * @throws InvalidArgumentException In case the given backend type is invalid
+ */
+ public function getBackendForm($type)
+ {
+ switch ($type) {
+ case 'db':
+ return new DbUserGroupBackendForm();
+ case 'ldap':
+ case 'msldap':
+ return new LdapUserGroupBackendForm();
+ default:
+ if (isset($this->customBackends[$type])) {
+ return new $this->customBackends[$type]();
+ }
+
+ throw new InvalidArgumentException(
+ sprintf($this->translate('Invalid backend type "%s" provided'), $type)
+ );
+ }
+ }
+
+ /**
+ * Populate the form with the given backend's config
+ *
+ * @param string $name
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function load($name)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $this->backendToLoad = $name;
+ return $this;
+ }
+
+ /**
+ * Add a new user group backend
+ *
+ * The backend to add is identified by the array-key `name'.
+ *
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws InvalidArgumentException In case $data does not contain a backend name
+ * @throws IcingaException In case a backend with the same name already exists
+ */
+ public function add(array $data)
+ {
+ if (! isset($data['name'])) {
+ throw new InvalidArgumentException('Key \'name\' missing');
+ }
+
+ $backendName = $data['name'];
+ if ($this->config->hasSection($backendName)) {
+ throw new IcingaException('A user group backend with the name "%s" does already exist', $backendName);
+ }
+
+ unset($data['name']);
+ $this->config->setSection($backendName, $data);
+ return $this;
+ }
+
+ /**
+ * Edit a user group backend
+ *
+ * @param string $name
+ * @param array $data
+ *
+ * @return $this
+ *
+ * @throws NotFoundError In case no backend with the given name is found
+ */
+ public function edit($name, array $data)
+ {
+ if (! $this->config->hasSection($name)) {
+ throw new NotFoundError('No user group backend called "%s" found', $name);
+ }
+
+ $backendConfig = $this->config->getSection($name);
+ if (isset($data['name'])) {
+ if ($data['name'] !== $name) {
+ $this->config->removeSection($name);
+ $name = $data['name'];
+ }
+
+ unset($data['name']);
+ }
+
+ $this->config->setSection($name, $backendConfig->merge($data));
+ return $this;
+ }
+
+ /**
+ * Remove a user group backend
+ *
+ * @param string $name
+ *
+ * @return $this
+ */
+ public function delete($name)
+ {
+ $this->config->removeSection($name);
+ return $this;
+ }
+
+ /**
+ * Create and add elements to this form
+ *
+ * @param array $formData
+ */
+ public function createElements(array $formData)
+ {
+ $backendTypes = array(
+ 'db' => $this->translate('Database'),
+ 'ldap' => 'LDAP',
+ 'msldap' => 'ActiveDirectory'
+ );
+
+ $customBackendTypes = array_keys($this->customBackends);
+ $backendTypes += array_combine($customBackendTypes, $customBackendTypes);
+
+ $backendType = isset($formData['type']) ? $formData['type'] : null;
+ if ($backendType === null) {
+ $backendType = key($backendTypes);
+ }
+
+ $this->addElement(
+ 'select',
+ 'type',
+ array(
+ 'ignore' => true,
+ 'required' => true,
+ 'autosubmit' => true,
+ 'label' => $this->translate('Backend Type'),
+ 'description' => $this->translate('The type of this user group backend'),
+ 'multiOptions' => $backendTypes
+ )
+ );
+
+ $this->addSubForm($this->getBackendForm($backendType)->create($formData), 'backend_form');
+ }
+
+ /**
+ * Populate the configuration of the backend to load
+ */
+ public function onRequest()
+ {
+ if ($this->backendToLoad) {
+ $data = $this->config->getSection($this->backendToLoad)->toArray();
+ $data['type'] = $data['backend'];
+ $data['name'] = $this->backendToLoad;
+ $this->populate($data);
+ }
+ }
+
+ /**
+ * Run the configured backend's inspection checks and show the result, if necessary
+ *
+ * This will only run any validation if the user pushed the 'backend_validation' button.
+ *
+ * @param array $formData
+ *
+ * @return bool
+ */
+ public function isValidPartial(array $formData)
+ {
+ if (isset($formData['backend_validation']) && parent::isValid($formData)) {
+ $inspection = static::inspectUserBackend($this);
+ if ($inspection !== null) {
+ $join = function ($e) use (&$join) {
+ return is_array($e) ? join("\n", array_map($join, $e)) : $e;
+ };
+ $this->addElement(
+ 'note',
+ 'inspection_output',
+ array(
+ 'order' => 0,
+ 'value' => '<strong>' . $this->translate('Validation Log') . "</strong>\n\n"
+ . join("\n", array_map($join, $inspection->toArray())),
+ 'decorators' => array(
+ 'ViewHelper',
+ array('HtmlTag', array('tag' => 'pre', 'class' => 'log-output')),
+ )
+ )
+ );
+
+ if ($inspection->hasError()) {
+ $this->warning(sprintf(
+ $this->translate('Failed to successfully validate the configuration: %s'),
+ $inspection->getError()
+ ));
+ return false;
+ }
+ }
+
+ $this->info($this->translate('The configuration has been successfully validated.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a submit button to this form and one to manually validate the configuration
+ *
+ * Calls parent::addSubmitButton() to add the submit button.
+ *
+ * @return $this
+ */
+ public function addSubmitButton()
+ {
+ parent::addSubmitButton()
+ ->getElement('btn_submit')
+ ->setDecorators(array('ViewHelper'));
+
+ $this->addElement(
+ 'submit',
+ 'backend_validation',
+ array(
+ 'ignore' => true,
+ 'label' => $this->translate('Validate Configuration'),
+ 'data-progress-label' => $this->translate('Validation In Progress'),
+ 'decorators' => array('ViewHelper')
+ )
+ );
+ $this->addDisplayGroup(
+ array('btn_submit', 'backend_validation'),
+ 'submit_validation',
+ array(
+ 'decorators' => array(
+ 'FormElements',
+ array('HtmlTag', array('tag' => 'div', 'class' => 'control-group form-controls'))
+ )
+ )
+ );
+
+ return $this;
+ }
+}
diff --git a/application/forms/Config/UserGroup/UserGroupForm.php b/application/forms/Config/UserGroup/UserGroupForm.php
new file mode 100644
index 0000000..b944e97
--- /dev/null
+++ b/application/forms/Config/UserGroup/UserGroupForm.php
@@ -0,0 +1,158 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Forms\Config\UserGroup;
+
+use Icinga\Application\Hook\ConfigFormEventsHook;
+use Icinga\Data\Filter\Filter;
+use Icinga\Forms\RepositoryForm;
+use Icinga\Web\Notification;
+
+class UserGroupForm extends RepositoryForm
+{
+ /**
+ * Create and add elements to this form to insert or update a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createInsertElements(array $formData)
+ {
+ $this->addElement(
+ 'text',
+ 'group_name',
+ array(
+ 'required' => true,
+ 'label' => $this->translate('Group Name')
+ )
+ );
+
+ if ($this->shouldInsert()) {
+ $this->setTitle($this->translate('Add a new group'));
+ $this->setSubmitLabel($this->translate('Add'));
+ } else { // $this->shouldUpdate()
+ $this->setTitle(sprintf($this->translate('Edit group %s'), $this->getIdentifier()));
+ $this->setSubmitLabel($this->translate('Save'));
+ }
+ }
+
+ /**
+ * Update a group
+ *
+ * @return bool
+ */
+ protected function onUpdateSuccess()
+ {
+ if (parent::onUpdateSuccess()) {
+ if (($newName = $this->getValue('group_name')) !== $this->getIdentifier()) {
+ $this->getRedirectUrl()->setParam('group', $newName);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create and add elements to this form to delete a group
+ *
+ * @param array $formData The data sent by the user
+ */
+ protected function createDeleteElements(array $formData)
+ {
+ $this->setTitle(sprintf($this->translate('Remove group %s?'), $this->getIdentifier()));
+ $this->addDescription($this->translate(
+ 'Note that all users that are currently a member of this group will'
+ . ' have their membership cleared automatically.'
+ ));
+ $this->setSubmitLabel($this->translate('Yes'));
+ $this->setAttrib('class', 'icinga-form icinga-controls');
+ }
+
+ /**
+ * Create and return a filter to use when updating or deleting a group
+ *
+ * @return Filter
+ */
+ protected function createFilter()
+ {
+ return Filter::where('group_name', $this->getIdentifier());
+ }
+
+ /**
+ * Return a notification message to use when inserting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getInsertMessage($success)
+ {
+ if ($success) {
+ return $this->translate('Group added successfully');
+ } else {
+ return $this->translate('Failed to add group');
+ }
+ }
+
+ /**
+ * Return a notification message to use when updating a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getUpdateMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been edited'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to edit group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ /**
+ * Return a notification message to use when deleting a group
+ *
+ * @param bool $success true or false, whether the operation was successful
+ *
+ * @return string
+ */
+ protected function getDeleteMessage($success)
+ {
+ if ($success) {
+ return sprintf($this->translate('Group "%s" has been removed'), $this->getIdentifier());
+ } else {
+ return sprintf($this->translate('Failed to remove group "%s"'), $this->getIdentifier());
+ }
+ }
+
+ public function isValid($formData)
+ {
+ $valid = parent::isValid($formData);
+
+ if ($valid && ConfigFormEventsHook::runIsValid($this) === false) {
+ foreach (ConfigFormEventsHook::getLastErrors() as $msg) {
+ $this->error($msg);
+ }
+
+ $valid = false;
+ }
+
+ return $valid;
+ }
+
+ public function onSuccess()
+ {
+ if (parent::onSuccess() === false) {
+ return false;
+ }
+
+ if (ConfigFormEventsHook::runOnSuccess($this) === false) {
+ Notification::error($this->translate(
+ 'Configuration successfully stored. Though, one or more module hooks failed to run.'
+ . ' See logs for details'
+ ));
+ }
+ }
+}