diff --git a/doc/accessibility/text-cue-for-required-form-control-labels.html b/doc/accessibility/text-cue-for-required-form-control-labels.html new file mode 100644 index 000000000..b9e624208 --- /dev/null +++ b/doc/accessibility/text-cue-for-required-form-control-labels.html @@ -0,0 +1,36 @@ + + + + + + Accessibility: Text cue for required form control labels + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/doc/installation.md b/doc/installation.md index d9483faef..aa85adb22 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -55,6 +55,14 @@ nginx: ./bin/icingacli setup config webserver nginx --document-root /usr/share/icingaweb2/public ```` +Save the output as new file in your webserver's configuration directory. + +Example for Apache on RHEL/CentOS: +```` +./bin/icingacli setup config webserver apache --document-root /usr/share/icingaweb2/public > /etc/httpd/conf.d/icingaweb2.conf +```` + + **Step 4: Preparing Web Setup** Because both web and CLI must have access to configuration and logs, permissions will be managed using a special diff --git a/icingaweb2.spec b/icingaweb2.spec index 1d97819d9..231b2642e 100644 --- a/icingaweb2.spec +++ b/icingaweb2.spec @@ -189,7 +189,7 @@ rm -rf %{buildroot} mkdir -p %{buildroot}/{%{basedir}/{modules,library,public},%{bindir},%{configdir}/modules/setup,%{logdir},%{phpdir},%{wwwconfigdir},%{_sysconfdir}/bash_completion.d,%{docsdir}} cp -prv application doc %{buildroot}/%{basedir} cp -pv etc/bash_completion.d/icingacli %{buildroot}/%{_sysconfdir}/bash_completion.d/icingacli -cp -prv modules/{monitoring,setup} %{buildroot}/%{basedir}/modules +cp -prv modules/{monitoring,setup,doc,translation} %{buildroot}/%{basedir}/modules cp -prv library/Icinga %{buildroot}/%{phpdir} cp -prv library/vendor %{buildroot}/%{basedir}/library cp -prv public/{css,img,js,error_norewrite.html} %{buildroot}/%{basedir}/public diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index fd7e39f54..016512ab4 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -5,6 +5,7 @@ namespace Icinga\Authentication\Backend; use Icinga\User; use Icinga\Authentication\UserBackend; +use Icinga\Protocol\Ldap\Query; use Icinga\Protocol\Ldap\Connection; use Icinga\Exception\AuthenticationException; use Icinga\Protocol\Ldap\Exception as LdapException; @@ -15,7 +16,7 @@ class LdapUserBackend extends UserBackend * Connection to the LDAP server * * @var Connection - **/ + */ protected $conn; protected $baseDn; @@ -36,7 +37,9 @@ class LdapUserBackend extends UserBackend } /** - * @return \Icinga\Protocol\Ldap\Query + * Create a query to select all usernames + * + * @return Query */ protected function selectUsers() { @@ -49,18 +52,18 @@ class LdapUserBackend extends UserBackend } /** - * Create query + * Create a query filtered by the given username * - * @param string $username + * @param string $username * - * @return \Icinga\Protocol\Ldap\Query - **/ + * @return Query + */ protected function selectUser($username) { - return $this->selectUsers()->where( - $this->userNameAttribute, - str_replace('*', '', $username) - ); + return $this->selectUsers()->setUsePagedResults(false)->where( + $this->userNameAttribute, + str_replace('*', '', $username) + ); } /** @@ -68,23 +71,22 @@ class LdapUserBackend extends UserBackend * * Try to bind to the backend and query all available users to check if: * * - * @throws AuthenticationException When authentication is not possible + * @throws AuthenticationException When authentication is not possible */ public function assertAuthenticationPossible() { try { - $q = $this->conn->select()->setBase($this->baseDn)->from($this->userClass); - $result = $q->fetchRow(); + $result = $this->selectUsers()->fetchRow(); } catch (LdapException $e) { throw new AuthenticationException('Connection not possible.', $e); } - if (! isset($result)) { + if ($result === null) { throw new AuthenticationException( 'No objects with objectClass="%s" in DN="%s" found.', $this->userClass, @@ -139,17 +141,16 @@ class LdapUserBackend extends UserBackend } /** - * Test whether the given user exists + * Return whether the given user exists * - * @param User $user + * @param User $user * * @return bool - * @throws AuthenticationException */ public function hasUser(User $user) { $username = $user->getUsername(); - $entry = $this->conn->fetchOne($this->selectUser($username)); + $entry = $this->selectUser($username)->fetchOne(); if (is_array($entry)) { return in_array(strtolower($username), array_map('strtolower', $entry)); @@ -159,24 +160,22 @@ class LdapUserBackend extends UserBackend } /** - * Authenticate the given user and return true on success, false on failure and null on error + * Return whether the given user credentials are valid * * @param User $user * @param string $password - * @param boolean $healthCheck Perform additional health checks to generate more useful exceptions in case - * of a configuration or backend error + * @param boolean $healthCheck Assert that authentication is possible at all * - * @return bool True when the authentication was successful, false when the username - * or password was invalid - * @throws AuthenticationException When an error occurred during authentication and authentication is not possible + * @return bool + * + * @throws AuthenticationException In case an error occured or the health check has failed */ - public function authenticate(User $user, $password, $healthCheck = true) + public function authenticate(User $user, $password, $healthCheck = false) { if ($healthCheck) { try { $this->assertAuthenticationPossible(); } catch (AuthenticationException $e) { - // Authentication not possible throw new AuthenticationException( 'Authentication against backend "%s" not possible.', $this->getName(), @@ -184,24 +183,27 @@ class LdapUserBackend extends UserBackend ); } } + if (! $this->hasUser($user)) { return false; } + try { $userDn = $this->conn->fetchDN($this->selectUser($user->getUsername())); $authenticated = $this->conn->testCredentials( $userDn, $password ); + if ($authenticated) { $groups = $this->getGroups($userDn); if ($groups !== null) { $user->setGroups($groups); } } + return $authenticated; } catch (LdapException $e) { - // Error during authentication of this specific user throw new AuthenticationException( 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', $user->getUsername(), @@ -238,6 +240,7 @@ class LdapUserBackend extends UserBackend $users[] = $row->{$this->userNameAttribute}; } } + return $users; } } diff --git a/library/Icinga/Authentication/Manager.php b/library/Icinga/Authentication/Manager.php index 81221176e..993475636 100644 --- a/library/Icinga/Authentication/Manager.php +++ b/library/Icinga/Authentication/Manager.php @@ -84,7 +84,7 @@ class Manager $preferences = new Preferences(); } $user->setPreferences($preferences); - $groups = array(); + $groups = $user->getGroups(); foreach (Config::app('groups') as $name => $config) { try { $groupBackend = UserGroupBackend::create($name, $config); diff --git a/library/Icinga/Authentication/UserBackend.php b/library/Icinga/Authentication/UserBackend.php index b8bde164d..cb6eb458b 100644 --- a/library/Icinga/Authentication/UserBackend.php +++ b/library/Icinga/Authentication/UserBackend.php @@ -93,10 +93,10 @@ abstract class UserBackend implements Countable break; case 'msldap': $groupOptions = array( - 'group_base_dn' => $backendConfig->group_base_dn, - 'group_attribute' => $backendConfig->group_attribute, - 'group_member_attribute' => $backendConfig->group_member_attribute, - 'group_class' => $backendConfig->group_class + 'group_base_dn' => $backendConfig->get('group_base_dn', $resource->getDN()), + 'group_attribute' => $backendConfig->get('group_attribute', 'sAMAccountName'), + 'group_member_attribute' => $backendConfig->get('group_member_attribute', 'member'), + 'group_class' => $backendConfig->get('group_class', 'group') ); $backend = new LdapUserBackend( $resource, diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index db55858e9..6db88365a 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -32,6 +32,8 @@ class Connection { const LDAP_NO_SUCH_OBJECT = 32; const LDAP_SIZELIMIT_EXCEEDED = 4; + const LDAP_ADMINLIMIT_EXCEEDED = 11; + const PAGE_SIZE = 1000; protected $ds; protected $hostname; @@ -98,9 +100,6 @@ class Connection protected $namingContexts; protected $discoverySuccess = false; - protected $lastResult; - protected $pageCookie; - /** * Constructor * @@ -171,11 +170,9 @@ class Connection return false; } throw new LdapException( - sprintf( - 'LDAP list for "%s" failed: %s', - $dn, - ldap_error($this->ds) - ) + 'LDAP list for "%s" failed: %s', + $dn, + ldap_error($this->ds) ); } $children = ldap_get_entries($this->ds, $result); @@ -183,7 +180,7 @@ class Connection $result = $this->deleteRecursively($children[$i]['dn']); if (!$result) { //return result code, if delete fails - throw new LdapException(sprintf('Recursively deleting "%s" failed', $dn)); + throw new LdapException('Recursively deleting "%s" failed', $dn); } } return $this->deleteDN($dn); @@ -200,11 +197,9 @@ class Connection return false; } throw new LdapException( - sprintf( - 'LDAP delete for "%s" failed: %s', - $dn, - ldap_error($this->ds) - ) + 'LDAP delete for "%s" failed: %s', + $dn, + ldap_error($this->ds) ); } @@ -225,10 +220,8 @@ class Connection $rows = $this->fetchAll($query, $fields); if (count($rows) !== 1) { throw new LdapException( - sprintf( - 'Cannot fetch single DN for %s', - $query->create() - ) + 'Cannot fetch single DN for %s', + $query->create() ); } return key($rows); @@ -244,6 +237,7 @@ class Connection { $query = clone $query; $query->limit(1); + $query->setUsePagedResults(false); $results = $this->fetchAll($query, $fields); return array_shift($results); } @@ -273,35 +267,144 @@ class Connection $this->connect(); $this->bind(); - $offset = $limit = null; - if ($query->hasLimit()) { - $offset = $query->getOffset(); - $limit = $query->getLimit(); + if ($query->getUsePagedResults() && version_compare(PHP_VERSION, '5.4.0') >= 0) { + return $this->runPagedQuery($query, $fields); + } else { + return $this->runQuery($query, $fields); + } + } + + protected function runQuery(Query $query, $fields = array()) + { + $limit = $query->getLimit(); + $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; + + $results = @ldap_search( + $this->ds, + $query->hasBase() ? $query->getBase() : $this->root_dn, + $query->create(), + empty($fields) ? $query->listFields() : $fields, + 0, // Attributes and values + $limit ? $offset + $limit : 0 + ); + if ($results === false) { + if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) { + return array(); + } + + throw new LdapException( + 'LDAP query "%s" (base %s) failed. Error: %s', + $query->create(), + $query->hasBase() ? $query->getBase() : $this->root_dn, + ldap_error($this->ds) + ); + } elseif (ldap_count_entries($this->ds, $results) === 0) { + return array(); + } + + foreach ($query->getSortColumns() as $col) { + ldap_sort($this->ds, $results, $col[0]); } $count = 0; $entries = array(); - $results = $this->runQuery($query, $fields); - while (! empty($results)) { + $entry = ldap_first_entry($this->ds, $results); + do { + $count += 1; + if ($offset === 0 || $offset < $count) { + $entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes( + ldap_get_attributes($this->ds, $entry) + ); + } + } while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry))); + + ldap_free_result($results); + return $entries; + } + + protected function runPagedQuery(Query $query, $fields = array()) + { + $limit = $query->getLimit(); + $offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; + $queryString = $query->create(); + $base = $query->hasBase() ? $query->getBase() : $this->root_dn; + + if (empty($fields)) { + $fields = $query->listFields(); + } + + $count = 0; + $cookie = ''; + $entries = array(); + do { + ldap_control_paged_result($this->ds, static::PAGE_SIZE, true, $cookie); + $results = @ldap_search($this->ds, $base, $queryString, $fields, 0, $limit ? $offset + $limit : 0); + if ($results === false) { + if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) { + break; + } + + throw new LdapException( + 'LDAP query "%s" (base %s) failed. Error: %s', + $queryString, + $base, + ldap_error($this->ds) + ); + } elseif (ldap_count_entries($this->ds, $results) === 0) { + if (in_array( + ldap_errno($this->ds), + array(static::LDAP_SIZELIMIT_EXCEEDED, static::LDAP_ADMINLIMIT_EXCEEDED) + )) { + Logger::warning( + 'Unable to request more than %u results. Does the server allow paged search requests? (%s)', + $count, + ldap_error($this->ds) + ); + } + + break; + } + $entry = ldap_first_entry($this->ds, $results); - while ($entry) { - $count++; - if ( - ($offset === null || $offset <= $count) - && ($limit === null || $limit > count($entries)) - ) { + do { + $count += 1; + if ($offset === 0 || $offset < $count) { $entries[ldap_get_dn($this->ds, $entry)] = $this->cleanupAttributes( ldap_get_attributes($this->ds, $entry) ); } + } while (($limit === 0 || $limit !== count($entries)) && ($entry = ldap_next_entry($this->ds, $entry))); - $entry = ldap_next_entry($this->ds, $entry); + try { + ldap_control_paged_result_response($this->ds, $results, $cookie); + } catch (Exception $e) { + // If the page size is greater than or equal to the sizeLimit value, the server should ignore the + // control as the request can be satisfied in a single page: https://www.ietf.org/rfc/rfc2696.txt + // This applies no matter whether paged search requests are permitted or not. You're done once you + // got everything you were out for. + if (count($entries) !== $limit) { + Logger::warning( + 'Unable to request paged LDAP results. Does the server allow paged search requests? (%s)', + $e->getMessage() + ); + } } - $results = $this->runQuery($query, $fields); + ldap_free_result($results); + } while ($cookie && ($limit === 0 || count($entries) < $limit)); + + if ($cookie) { + // A sequence of paged search requests is abandoned by the client sending a search request containing a + // pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by + // the server: https://www.ietf.org/rfc/rfc2696.txt + ldap_control_paged_result($this->ds, 0, false, $cookie); + ldap_search($this->ds, $base, $queryString, $fields); // Returns no entries, due to the page size + } else { + // Reset the paged search request so that subsequent requests succeed + ldap_control_paged_result($this->ds, 0); } - return $entries; + return $entries; // TODO(7693): Sort entries post-processed } protected function cleanupAttributes($attrs) @@ -320,79 +423,6 @@ class Connection return $clean; } - protected function runQuery(Query $query, $fields = array()) - { - if ($query->getUsePagedResults() && version_compare(PHP_VERSION, '5.4.0') >= 0) { - if ($this->pageCookie === null) { - $this->pageCookie = ''; - } else { - try { - ldap_control_paged_result_response($this->ds, $this->lastResult, $this->pageCookie); - } catch (Exception $e) { - $this->pageCookie = ''; - if (! $query->hasLimit() || ldap_errno($this->ds) !== static::LDAP_SIZELIMIT_EXCEEDED) { - Logger::error( - 'Unable to request paged LDAP results. Does the server allow paged search requests? (%s)', - $e->getMessage() - ); - } - } - - ldap_free_result($this->lastResult); - if (! $this->pageCookie) { - $this->pageCookie = $this->lastResult = null; - // Abandon the paged search request so that subsequent requests succeed - ldap_control_paged_result($this->ds, 0); - return false; - } - } - - // Does not matter whether we'll use a valid page size here, - // as the server applies its hard limit in case its too high - ldap_control_paged_result( - $this->ds, - $query->hasLimit() ? $query->getLimit() : 500, - true, - $this->pageCookie - ); - } elseif ($this->lastResult !== null) { - ldap_free_result($this->lastResult); - $this->lastResult = null; - return false; - } - - $base = $query->hasBase() ? $query->getBase() : $this->root_dn; - $results = @ldap_search( - $this->ds, - $base, - $query->create(), - empty($fields) ? $query->listFields() : $fields, - 0, // Attributes and values - $query->hasLimit() ? $query->getOffset() + $query->getLimit() : 0 // No limit - at least where possible - ); - - if ($results === false) { - if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) { - return false; - } - throw new LdapException( - sprintf( - 'LDAP query "%s" (root %s) failed: %s', - $query->create(), - $this->root_dn, - ldap_error($this->ds) - ) - ); - } - - foreach ($query->getSortColumns() as $col) { - ldap_sort($this->ds, $results, $col[0]); - } - - $this->lastResult = $results; - return $results; - } - public function testCredentials($username, $password) { $this->connect(); @@ -471,18 +501,14 @@ class Connection } else { Logger::debug('LDAP STARTTLS failed: %s', ldap_error($ds)); throw new LdapException( - sprintf( - 'LDAP STARTTLS failed: %s', - ldap_error($ds) - ) + 'LDAP STARTTLS failed: %s', + ldap_error($ds) ); } } elseif ($force_tls) { throw new LdapException( - sprintf( - 'TLS is required but not announced by %s', - $this->hostname - ) + 'TLS is required but not announced by %s', + $this->hostname ); } else { // TODO: Log noticy -> TLS enabled but not announced @@ -708,24 +734,20 @@ class Connection if (! $result) { throw new LdapException( - sprintf( - 'Capability query failed (%s:%d): %s. Check if hostname and port of the ldap resource are correct ' - . ' and if anonymous access is permitted.', - $this->hostname, - $this->port, - ldap_error($ds) - ) + 'Capability query failed (%s:%d): %s. Check if hostname and port of the' + . ' ldap resource are correct and if anonymous access is permitted.', + $this->hostname, + $this->port, + ldap_error($ds) ); } $entry = ldap_first_entry($ds, $result); if ($entry === false) { throw new LdapException( - sprintf( - 'Capabilities not available (%s:%d): %s. Discovery of root DSE probably not permitted.', - $this->hostname, - $this->port, - ldap_error($ds) - ) + 'Capabilities not available (%s:%d): %s. Discovery of root DSE probably not permitted.', + $this->hostname, + $this->port, + ldap_error($ds) ); } @@ -771,14 +793,12 @@ class Connection $r = @ldap_bind($this->ds, $this->bind_dn, $this->bind_pw); if (! $r) { throw new LdapException( - sprintf( - 'LDAP connection to %s:%s (%s / %s) failed: %s', - $this->hostname, - $this->port, - $this->bind_dn, - '***' /* $this->bind_pw */, - ldap_error($this->ds) - ) + 'LDAP connection to %s:%s (%s / %s) failed: %s', + $this->hostname, + $this->port, + $this->bind_dn, + '***' /* $this->bind_pw */, + ldap_error($this->ds) ); } $this->bound = true; diff --git a/library/Icinga/Protocol/Ldap/Exception.php b/library/Icinga/Protocol/Ldap/Exception.php index 784b84a9d..de7a10651 100644 --- a/library/Icinga/Protocol/Ldap/Exception.php +++ b/library/Icinga/Protocol/Ldap/Exception.php @@ -3,10 +3,12 @@ namespace Icinga\Protocol\Ldap; +use Icinga\Exception\IcingaException; + /** * Class Exception * @package Icinga\Protocol\Ldap */ -class Exception extends \Exception +class Exception extends IcingaException { } diff --git a/library/Icinga/Protocol/Ldap/Query.php b/library/Icinga/Protocol/Ldap/Query.php index 8b71fc918..897f12f4e 100644 --- a/library/Icinga/Protocol/Ldap/Query.php +++ b/library/Icinga/Protocol/Ldap/Query.php @@ -27,8 +27,8 @@ class Query protected $connection; protected $filters = array(); protected $fields = array(); - protected $limit_count; - protected $limit_offset; + protected $limit_count = 0; + protected $limit_offset = 0; protected $sort_columns = array(); protected $count; protected $base; @@ -111,7 +111,7 @@ class Query */ public function hasLimit() { - return $this->limit_count !== null; + return $this->limit_count > 0; } /** diff --git a/modules/monitoring/application/views/helpers/PluginOutput.php b/modules/monitoring/application/views/helpers/PluginOutput.php index e6c8b2d5a..d494a7810 100644 --- a/modules/monitoring/application/views/helpers/PluginOutput.php +++ b/modules/monitoring/application/views/helpers/PluginOutput.php @@ -5,34 +5,44 @@ class Zend_View_Helper_PluginOutput extends Zend_View_Helper_Abstract { protected static $purifier; + protected static $txtPatterns = array( + '~\\\n~', + '~\\\t~', + '~\\\n\\\n~', + '~(\[|\()OK(\]|\))~', + '~(\[|\()WARNING(\]|\))~', + '~(\[|\()CRITICAL(\]|\))~', + '~(\[|\()UNKNOWN(\]|\))~', + '~\@{6,}~' + ); + + protected static $txtReplacements = array( + "\n", + "\t", + "\n", + '$1OK$2', + '$1WARNING$2', + '$1CRITICAL$2', + '$1UNKNOWN$2', + '@@@@@@', + ); + public function pluginOutput($output) { if (empty($output)) { return ''; } $output = preg_replace('~]+>~', "\n", $output); - if (preg_match('~<\w+[^>]*>~', $output)) { + if (preg_match('~<\w+[^>^\\\]{,60}>~', $output)) { // HTML $output = preg_replace('~getPurifier()->purify($output) ); - } elseif (preg_match('~\\\n~', $output)) { - // Plaintext - $output = '
'
-               . preg_replace(
-              '~\\\n~', "\n", preg_replace(
-                '~\\\n\\\n~', "\n",
-                preg_replace('~\[OK\]~', '[OK]',
-                 preg_replace('~\[WARNING\]~', '[WARNING]',
-                  preg_replace('~\[CRITICAL\]~', '[CRITICAL]',
-                   preg_replace('~\@{6,}~', '@@@@@@',
-                     $this->view->escape($output)
-                ))))
-              )
-            ) . '
'; } else { - $output = '
'
-               . preg_replace('~\@{6,}~', '@@@@@@',
+            // Plaintext
+            $output = '
' . preg_replace(
+                self::$txtPatterns,
+                self::$txtReplacements,
                 $this->view->escape($output)
             ) . '
'; } @@ -55,7 +65,7 @@ class Zend_View_Helper_PluginOutput extends Zend_View_Helper_Abstract parse_str($m[1], $params); if (isset($params['host'])) { $tag->setAttribute('href', $this->view->baseUrl( - '/monitoring/detail/show?host=' . urlencode($params['host'] + '/monitoring/host/show?host=' . urlencode($params['host'] ))); } } else { diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 6a82a6ee5..92aa1e9d8 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -737,7 +737,6 @@ } this.icinga.ui.assignUniqueContainerIds(); - console.log(origFocus); if (origFocus.length == origFocus[0] !== '') { setTimeout(function() { $(self.icinga.utils.getElementByDomPath(origFocus)).focus(); diff --git a/public/js/icinga/utils.js b/public/js/icinga/utils.js index bd64e586e..4b04b3d44 100644 --- a/public/js/icinga/utils.js +++ b/public/js/icinga/utils.js @@ -284,10 +284,8 @@ if (! $element) { $element = $(selector); } else { - console.log(selector); $element = $element.children(selector).first(); if (! $element[0]) { - console.log("element not existing stopping..."); return false; } }