From 8ca6cc32b2c789a3149861159ad258f2cb9491e3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 14:39:39 +0200 Subject: Adding upstream version 2.11.4. Signed-off-by: Daniel Baumann --- .../forms/Config/General/ApplicationConfigForm.php | 93 ++++ .../DefaultAuthenticationDomainConfigForm.php | 46 ++ .../forms/Config/General/LoggingConfigForm.php | 142 ++++++ .../forms/Config/General/ThemingConfigForm.php | 78 ++++ application/forms/Config/GeneralConfigForm.php | 40 ++ .../forms/Config/Resource/DbResourceForm.php | 238 ++++++++++ .../forms/Config/Resource/FileResourceForm.php | 67 +++ .../forms/Config/Resource/LdapResourceForm.php | 129 ++++++ .../forms/Config/Resource/SshResourceForm.php | 148 +++++++ application/forms/Config/ResourceConfigForm.php | 441 +++++++++++++++++++ .../forms/Config/User/CreateMembershipForm.php | 191 ++++++++ application/forms/Config/User/UserForm.php | 210 +++++++++ .../forms/Config/UserBackend/DbBackendForm.php | 82 ++++ .../Config/UserBackend/ExternalBackendForm.php | 83 ++++ .../forms/Config/UserBackend/LdapBackendForm.php | 414 ++++++++++++++++++ application/forms/Config/UserBackendConfigForm.php | 482 +++++++++++++++++++++ .../forms/Config/UserBackendReorderForm.php | 86 ++++ .../forms/Config/UserGroup/AddMemberForm.php | 182 ++++++++ .../Config/UserGroup/DbUserGroupBackendForm.php | 79 ++++ .../Config/UserGroup/LdapUserGroupBackendForm.php | 370 ++++++++++++++++ .../Config/UserGroup/UserGroupBackendForm.php | 314 ++++++++++++++ .../forms/Config/UserGroup/UserGroupForm.php | 158 +++++++ 22 files changed, 4073 insertions(+) create mode 100644 application/forms/Config/General/ApplicationConfigForm.php create mode 100644 application/forms/Config/General/DefaultAuthenticationDomainConfigForm.php create mode 100644 application/forms/Config/General/LoggingConfigForm.php create mode 100644 application/forms/Config/General/ThemingConfigForm.php create mode 100644 application/forms/Config/GeneralConfigForm.php create mode 100644 application/forms/Config/Resource/DbResourceForm.php create mode 100644 application/forms/Config/Resource/FileResourceForm.php create mode 100644 application/forms/Config/Resource/LdapResourceForm.php create mode 100644 application/forms/Config/Resource/SshResourceForm.php create mode 100644 application/forms/Config/ResourceConfigForm.php create mode 100644 application/forms/Config/User/CreateMembershipForm.php create mode 100644 application/forms/Config/User/UserForm.php create mode 100644 application/forms/Config/UserBackend/DbBackendForm.php create mode 100644 application/forms/Config/UserBackend/ExternalBackendForm.php create mode 100644 application/forms/Config/UserBackend/LdapBackendForm.php create mode 100644 application/forms/Config/UserBackendConfigForm.php create mode 100644 application/forms/Config/UserBackendReorderForm.php create mode 100644 application/forms/Config/UserGroup/AddMemberForm.php create mode 100644 application/forms/Config/UserGroup/DbUserGroupBackendForm.php create mode 100644 application/forms/Config/UserGroup/LdapUserGroupBackendForm.php create mode 100644 application/forms/Config/UserGroup/UserGroupBackendForm.php create mode 100644 application/forms/Config/UserGroup/UserGroupForm.php (limited to 'application/forms/Config') 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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( + '%3$s', + $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 @@ +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' => '' . $this->translate('Validation Log') . "\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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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' => '' . $this->translate('Validation Log') . "\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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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' => '' . $this->translate('Validation Log') . "\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 @@ +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' + )); + } + } +} -- cgit v1.2.3