From 4587f26476c10a48a74055e4ffaf745c970bae82 Mon Sep 17 00:00:00 2001 From: Marius Hein Date: Fri, 6 Jun 2014 14:41:57 +0200 Subject: [PATCH 01/29] Revert "Ui/Sparklines: Remove img src before putting it into DOM" This reverts commit a75796c64d397d37f1d41ae3d7e6bdf119b340d9. --- public/js/icinga/loader.js | 2 +- public/js/icinga/ui.js | 16 ---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 338629136..3b92e5b2f 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -269,7 +269,7 @@ if (this.processRedirectHeader(req)) return; // div helps getting an XML tree - var $resp = $('
' + icinga.ui.removeImageSourceFromSparklines(req.responseText) + '
'); + var $resp = $('
' + req.responseText + '
'); var active = false; var rendered = false; var classes; diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js index 6902953ca..e76e2dba8 100644 --- a/public/js/icinga/ui.js +++ b/public/js/icinga/ui.js @@ -657,22 +657,6 @@ ); }, - /** - * Find all svg charts and removes src attributes for sparklines - * - * @param {string} text - * @returns {string} - */ - removeImageSourceFromSparklines: function(text) { - var match, sourceMatch; - var re = new RegExp(/(src=".+chart.php[^"]+")/g); - var reSource = new RegExp(/src="([^"]+)"/); - while ((match = re.exec(text))) { - text = text.replace(match[0], ''); - } - return text; - }, - initializeControls: function (parent) { var self = this; From 06296f29d8d74b4122e245d01371760e2af3df6e Mon Sep 17 00:00:00 2001 From: Marius Hein Date: Fri, 6 Jun 2014 16:35:33 +0200 Subject: [PATCH 02/29] UI/Sparklines: Change sparkline code to serverside only fixes #6124 --- library/Icinga/Web/Widget/Chart/InlinePie.php | 4 +++ public/js/icinga/events.js | 4 --- public/js/icinga/loader.js | 5 ---- public/js/icinga/ui.js | 26 ------------------- 4 files changed, 4 insertions(+), 35 deletions(-) diff --git a/library/Icinga/Web/Widget/Chart/InlinePie.php b/library/Icinga/Web/Widget/Chart/InlinePie.php index 48416b8ec..c219ab4d7 100644 --- a/library/Icinga/Web/Widget/Chart/InlinePie.php +++ b/library/Icinga/Web/Widget/Chart/InlinePie.php @@ -50,10 +50,14 @@ class InlinePie extends AbstractWidget * @var string */ private $template =<<<'EOD' + + EOD; /** diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js index e8bfdcfc6..1b0178d31 100644 --- a/public/js/icinga/events.js +++ b/public/js/icinga/events.js @@ -69,10 +69,6 @@ $('input.autofocus', el).focus(); - $('img.inlinepie', el).each(function() { - icinga.ui.initializeSparklines($(this)); - }); - // replace all sparklines $('span.sparkline', el).sparkline('html', { enableTagOptions: true }); diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 3b92e5b2f..918b890d8 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -409,11 +409,6 @@ this.icinga.ui.initializeTriStates($resp); - // Replace images with sparklines. - $resp.find('img.inlinepie').each(function(){ - self.icinga.ui.initializeSparklines($(this)); - }); - /* Should we try to fiddle with responses containing full HTML? */ /* if ($('body', $resp).length) { diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js index e76e2dba8..cca166126 100644 --- a/public/js/icinga/ui.js +++ b/public/js/icinga/ui.js @@ -631,32 +631,6 @@ } }, - /** - * Search and replace all inlinepies with html for sparklines. - * - * @param parent - */ - initializeSparklines: function($container) { - - // replace all remaining images with sparklines - var title = $container.attr('title'), - values = $container.data('icinga-values'), - colors = $container.data('icinga-colors'), - width = $container.css('width'), - height = $container.css('height'); - if (!values) { - return; - } - $container.replaceWith( - '' - ); - }, - initializeControls: function (parent) { var self = this; From 953d22244a7cd81b188d1263758c6b88af4aa3fc Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Fri, 6 Jun 2014 15:39:01 +0200 Subject: [PATCH 03/29] Add host discovery to authentication page Find all domains for a given hostname. refs #6093 Conflicts: application/forms/Install/AuthenticationPage.php --- library/Icinga/Protocol/Ldap/Connection.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 13ae19fd9..70c97055d 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -353,10 +353,18 @@ class Connection return $dir; } - protected function discoverServerlistForDomain($domain) + public static function discoverServerlistForDomain($domain) { + $domains = array(); $ldaps_records = dns_get_record('_ldaps._tcp.' . $domain, DNS_SRV); + foreach ($ldaps_records as $record) { + $domains[$record['target']] = true; + } $ldap_records = dns_get_record('_ldap._tcp.' . $domain, DNS_SRV); + foreach ($ldap_records as $record) { + $domains[$record['target']] = true; + } + return array_keys($domains); } protected function prepareNewConnection() From efe67377a83daffa21344bc2ba4e78b31ae01653 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Fri, 6 Jun 2014 17:45:24 +0200 Subject: [PATCH 04/29] Move dns discovery functions into separate class Add functions to lookup and reverse-lookup domain names and move the ldap discovery function into a separate class refs #6093 --- library/Icinga/Protocol/Dns.php | 114 ++++++++++++++++++++ library/Icinga/Protocol/Ldap/Connection.php | 14 --- 2 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 library/Icinga/Protocol/Dns.php diff --git a/library/Icinga/Protocol/Dns.php b/library/Icinga/Protocol/Dns.php new file mode 100644 index 000000000..b2807ac5d --- /dev/null +++ b/library/Icinga/Protocol/Dns.php @@ -0,0 +1,114 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + * + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\Protocol; + +/** + * Discover dns records using regular or reverse lookup + */ +class Dns { + + /** + * Get all ldap records for the given domain + * + * @param String $query The domain to query + * + * @return array An array of entries + */ + public static function ldapRecords($query) + { + $ldaps_records = dns_get_record('_ldaps._tcp.' . $query); + $ldap_records = dns_get_record('_ldap._tcp.' . $query); + return array_merge($ldaps_records, $ldap_records); + } + + /** + * Get all ldap records for the given domain + * + * @param String $query The domain to query + * @param int $type The type of DNS-entry to fetch, see http://www.php.net/manual/de/function.dns-get-record.php + * for available types + * + * @return array|Boolean An array of entries + */ + public static function records($query, $type = DNS_ANY) + { + return dns_get_record($query, $type); + } + + /** + * Reverse lookup all hostname on the given ip address + * + * @param $ipAddress + * @param int $type + * + * @return array|Boolean + */ + public static function ptr($ipAddress, $type = DNS_ANY) + { + $host = gethostbyaddr($ipAddress); + if ($host === false || $host === $ipAddress) { + // malformed input or no host found + return false; + } + return self::records($host, $type); + } + + /** + * Get the IPv4 address of the given hostname. + * + * @param $hostname The hostname to resolve + * + * @return String|Boolean The IPv4 address of the given hostname, or false when no entry exists. + */ + public static function ipv4($hostname) + { + $records = dns_get_record($hostname, DNS_A); + if ($records !== false && sizeof($records) > 0) { + return $records[0]['ip']; + } + return false; + } + + /** + * Get the IPv6 address of the given hostname. + * + * @param $hostname The hostname to resolve + * + * @return String|Boolean The IPv6 address of the given hostname, or false when no entry exists. + */ + public static function ipv6($hostname) + { + $records = dns_get_record($hostname, DNS_AAAA); + if ($records !== false && sizeof($records) > 0) { + return $records[0]['ip']; + } + return false; + } +} \ No newline at end of file diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 70c97055d..54cc05f53 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -353,20 +353,6 @@ class Connection return $dir; } - public static function discoverServerlistForDomain($domain) - { - $domains = array(); - $ldaps_records = dns_get_record('_ldaps._tcp.' . $domain, DNS_SRV); - foreach ($ldaps_records as $record) { - $domains[$record['target']] = true; - } - $ldap_records = dns_get_record('_ldap._tcp.' . $domain, DNS_SRV); - foreach ($ldap_records as $record) { - $domains[$record['target']] = true; - } - return array_keys($domains); - } - protected function prepareNewConnection() { $use_tls = false; From 305a025e7adb58d2573f4af2d6a3a7f203d3d684 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Fri, 6 Jun 2014 17:57:50 +0200 Subject: [PATCH 05/29] Detect ldap default naming context Read the entries in the RootDTS of the given ldap server, to discover its default naming context and capabilities refs #6097 refs #6096 --- library/Icinga/Protocol/Ldap/Connection.php | 179 +++++++++++++++----- 1 file changed, 136 insertions(+), 43 deletions(-) diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 54cc05f53..6419fbb6f 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -63,6 +63,7 @@ class Connection protected $bind_pw; protected $root_dn; protected $count; + protected $ldap_extension = array( '1.3.6.1.4.1.1466.20037' => 'STARTTLS', // '1.3.6.1.4.1.4203.1.11.1' => '11.1', // PASSWORD_MODIFY @@ -109,6 +110,8 @@ class Connection protected $supports_v3 = false; protected $supports_tls = false; + protected $capabilities; + /** * Constructor * @@ -365,6 +368,7 @@ class Connection $ds = ldap_connect($this->hostname, $this->port); $cap = $this->discoverCapabilities($ds); + $this->capabilities = $cap; if ($use_tls) { if ($cap->starttls) { @@ -399,7 +403,6 @@ class Connection // TODO: remove this -> FORCING v3 for now ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); - Logger::warning('No LDAPv3 support detected'); } @@ -429,6 +432,116 @@ class Connection } } + protected function hasCapabilityStarTSL($cap) + { + $cap = $this->getExtensionCapabilities($cap); + return isset($cap['1.3.6.1.4.1.1466.20037']); + } + + protected function hasCapabilityLdapV3($cap) + { + if ((is_string($cap->supportedLDAPVersion) + && (int) $cap->supportedLDAPVersion === 3) + || (is_array($cap->supportedLDAPVersion) + && in_array(3, $cap->supportedLDAPVersion) + )) { + return true; + } + return false; + } + + protected function getExtensionCapabilities($cap) + { + $extensions = array(); + if (isset($cap->supportedExtension)) { + foreach ($cap->supportedExtension as $oid) { + if (array_key_exists($oid, $this->ldap_extension)) { + if ($this->ldap_extension[$oid] === 'STARTTLS') { + $extensions['1.3.6.1.4.1.1466.20037'] = $this->ldap_extension['1.3.6.1.4.1.1466.20037']; + } + } + } + } + return $extensions; + } + + protected function getMsCapabilities($cap) + { + $ms = array(); + foreach ($this->ms_capability as $name) { + $ms[$this->convName($name)] = false; + } + + if (isset($cap->supportedCapabilities)) { + foreach ($cap->supportedCapabilities as $oid) { + if (array_key_exists($oid, $this->ms_capability)) { + $ms[$this->convName($this->ms_capability[$oid])] = true; + } + } + } + return (object)$ms; + } + + private function convName($name) + { + $parts = explode('_', $name); + foreach ($parts as $i => $part) { + $parts[$i] = ucfirst(strtolower($part)); + } + return implode('', $parts); + } + + /** + * Get the capabilities of this ldap server + * + * @return stdClass An object, providing the flags 'ldapv3' and 'starttls' to indicate LdapV3 and StartTLS + * support and an additional property 'msCapabilities', containing all supported active directory capabilities. + */ + public function getCapabilities() + { + return $this->capabilities; + } + + /** + * Get the default naming context of this ldap connection + * + * @return String|null the default naming context, or null when no contexts are available + */ + public function getDefaultNamingContext() + { + $cap = $this->capabilities; + if (isset($cap->defaultNamingContext)) { + return $cap->defaultNamingContext; + } + $namingContexts = $this->namingContexts($cap); + return empty($namingContexts) ? null : $namingContexts[0]; + } + + /** + * Fetch the namingContexts for this Ldap-Connection + * + * @return array the available naming contexts + */ + public function namingContexts() + { + $cap = $this->capabilities; + if (!isset($cap->namingContexts)) { + return array(); + } + if (!is_array($cap->namingContexts)) { + return array($cap->namingContexts); + } + return $cap->namingContexts; + } + + /** + * Discover the capabilities of the given ldap-server + * + * @param $ds The link identifier of the current ldap connection + * + * @return bool|object The capabilities or false if the server has none + * @throws Exception When the capability query fails + */ protected function discoverCapabilities($ds) { $query = $this->select()->from( @@ -471,69 +584,49 @@ class Connection $cap = (object) array( 'ldapv3' => false, 'starttls' => false, + 'msCapabilities' => array() ); if ($entry === false) { // TODO: Is it OK to have no capabilities? - return $cap; + return false; } $ldapAttributes = ldap_get_attributes($ds, $entry); - $result = $this->cleanupAttributes( - $ldapAttributes - ); + $result = $this->cleanupAttributes($ldapAttributes); + $cap->ldapv3 = $this->hasCapabilityLdapV3($result); + $cap->starttls = $this->hasCapabilityStarTSL($result); + $cap->msCapabilities = $this->getMsCapabilities($result); + $cap->namingContexts = $result->namingContexts; /* if (isset($result->dnsHostName)) { ldap_set_option($ds, LDAP_OPT_HOST_NAME, $result->dnsHostName); } */ - if ((is_string($result->supportedLDAPVersion) - && (int) $result->supportedLDAPVersion === 3) - || (is_array($result->supportedLDAPVersion) - && in_array(3, $result->supportedLDAPVersion) - )) { - $cap->ldapv3 = true; - } - - if (isset($result->supportedCapabilities)) { - foreach ($result->supportedCapabilities as $oid) { - if (array_key_exists($oid, $this->ms_capability)) { - // echo $this->ms_capability[$oid] . "\n"; - } - } - } - if (isset($result->supportedExtension)) { - foreach ($result->supportedExtension as $oid) { - if (array_key_exists($oid, $this->ldap_extension)) { - if ($this->ldap_extension[$oid] === 'STARTTLS') { - $cap->starttls = true; - } - } - } - } return $cap; } - public function connect() + public function connect($anonymous = false) { if ($this->ds !== null) { return; } $this->ds = $this->prepareNewConnection(); - $r = @ldap_bind($this->ds, $this->bind_dn, $this->bind_pw); - - if (! $r) { - throw new \Exception( - sprintf( - 'LDAP connection to %s:%s (%s / %s) failed: %s', - $this->hostname, - $this->port, - $this->bind_dn, - '***' /* $this->bind_pw */, - ldap_error($this->ds) - ) - ); + if (!$anonymous) { + $r = @ldap_bind($this->ds, $this->bind_dn, $this->bind_pw); + if (! $r) { + throw new \Exception( + sprintf( + 'LDAP connection to %s:%s (%s / %s) failed: %s', + $this->hostname, + $this->port, + $this->bind_dn, + '***' /* $this->bind_pw */, + ldap_error($this->ds) + ) + ); + } } } From fa797de05fb16fc297ef6e63bf60220b469027d6 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 11 Jun 2014 08:58:53 +0200 Subject: [PATCH 06/29] Fix invalid default logging configuration --- config/config.ini.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.ini.in b/config/config.ini.in index 35febdb3b..9b3fc8136 100644 --- a/config/config.ini.in +++ b/config/config.ini.in @@ -14,7 +14,7 @@ timeFormat = "g:i A" [logging] enable = true ; Writing to a Stream -type = "stream" +type = "file" ; Write data to the following file target = "@icingaweb_log_path@/icingaweb.log" ; Write data to a PHP stream @@ -22,8 +22,8 @@ target = "@icingaweb_log_path@/icingaweb.log" ; Writing to the System Log ;type = "syslog" -; Prefix all syslog messages generated with the string "Icinga Web" -;application = "Icinga Web" +; Prefix all syslog messages generated with the string "icingaweb" +;application = "icingaweb" ;facility = "LOG_USER" level = 1 From 159d765f14a2d18e5fd1dcc814ad4079c094bf36 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 11 Jun 2014 13:39:22 +0200 Subject: [PATCH 07/29] Fix that calling ActionController::translate() throws an exception Translating strings must not throw an exception even if the given domain is not valid. fixes #6432 --- .../Application/ApplicationBootstrap.php | 3 +-- library/Icinga/Application/functions.php | 2 +- library/Icinga/Util/Translator.php | 6 ----- .../Web/Controller/ActionController.php | 2 +- test/php/regression/Bug6432Test.php | 23 +++++++++++++++++++ 5 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 test/php/regression/Bug6432Test.php diff --git a/library/Icinga/Application/ApplicationBootstrap.php b/library/Icinga/Application/ApplicationBootstrap.php index e8d407216..7e6fed800 100644 --- a/library/Icinga/Application/ApplicationBootstrap.php +++ b/library/Icinga/Application/ApplicationBootstrap.php @@ -30,7 +30,6 @@ namespace Icinga\Application; -use DateTimeZone; use Exception; use Zend_Config; use Icinga\Application\Modules\Manager as ModuleManager; @@ -484,7 +483,7 @@ abstract class ApplicationBootstrap $localeDir = $this->getApplicationDir('locale'); if (file_exists($localeDir) && is_dir($localeDir)) { - Translator::registerDomain('icinga', $localeDir); + Translator::registerDomain(Translator::DEFAULT_DOMAIN, $localeDir); } return $this; diff --git a/library/Icinga/Application/functions.php b/library/Icinga/Application/functions.php index b600028d6..0530b6af2 100644 --- a/library/Icinga/Application/functions.php +++ b/library/Icinga/Application/functions.php @@ -32,7 +32,7 @@ use \Icinga\Util\Translator; if (extension_loaded('gettext')) { function t($messageId) { - return Translator::translate($messageId, 'icinga'); + return Translator::translate($messageId, Translator::DEFAULT_DOMAIN); } function mt($domain, $messageId) diff --git a/library/Icinga/Util/Translator.php b/library/Icinga/Util/Translator.php index 733806c72..f868b3465 100644 --- a/library/Icinga/Util/Translator.php +++ b/library/Icinga/Util/Translator.php @@ -62,15 +62,9 @@ class Translator * @param string $domain The primary domain to use * * @return string The translated string - * - * @throws Exception In case the given domain is unknown */ public static function translate($text, $domain) { - if ($domain !== self::DEFAULT_DOMAIN && !array_key_exists($domain, self::$knownDomains)) { - throw new Exception("Cannot translate string '$text' with unknown domain '$domain'"); - } - $res = dgettext($domain, $text); if ($res === $text && $domain !== self::DEFAULT_DOMAIN) { return dgettext(self::DEFAULT_DOMAIN, $text); diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php index 870078e96..7593db752 100644 --- a/library/Icinga/Web/Controller/ActionController.php +++ b/library/Icinga/Web/Controller/ActionController.php @@ -301,7 +301,7 @@ class ActionController extends Zend_Controller_Action public function translate($text) { $module = $this->getRequest()->getModuleName(); - $domain = $module === 'default' ? 'icinga' : $module; + $domain = $module === 'default' ? Translator::DEFAULT_DOMAIN : $module; return Translator::translate($text, $domain); } diff --git a/test/php/regression/Bug6432Test.php b/test/php/regression/Bug6432Test.php new file mode 100644 index 000000000..4194d9d41 --- /dev/null +++ b/test/php/regression/Bug6432Test.php @@ -0,0 +1,23 @@ +assertEquals('test', Translator::translate('test', 'invalid_domain')); + } +} From bca166c64432f35d2195a85e58fbdb0778e07371 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 11 Jun 2014 13:14:05 +0200 Subject: [PATCH 08/29] Do not throw an exception when the username does not exist refs #6457 --- .../Authentication/Backend/LdapUserBackend.php | 14 ++++++++------ library/Icinga/Protocol/Ldap/Connection.php | 15 +++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index d17cb3624..44445cec8 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -94,16 +94,18 @@ class LdapUserBackend extends UserBackend * @param User $user * @param string $password * - * @return bool|null - * @throws AuthenticationException + * @return bool True when the authentication was successful, false when the username or password was invalid + * @throws AuthenticationException When an error occurred during authentication */ public function authenticate(User $user, $password) { try { - return $this->conn->testCredentials( - $this->conn->fetchDN($this->createQuery($user->getUsername())), - $password - ); + $userDn = $this->conn->fetchDN($this->createQuery($user->getUsername())); + if (!$userDn) { + // User does not exist + return false; + } + return $this->conn->testCredentials($userDn, $password); } catch (Exception $e) { throw new AuthenticationException( sprintf( diff --git a/library/Icinga/Protocol/Ldap/Connection.php b/library/Icinga/Protocol/Ldap/Connection.php index 6419fbb6f..2611f5580 100644 --- a/library/Icinga/Protocol/Ldap/Connection.php +++ b/library/Icinga/Protocol/Ldap/Connection.php @@ -207,16 +207,19 @@ class Connection return true; } + /** + * Fetch the distinguished name of the first result of the given query + * + * @param $query + * @param array $fields + * + * @return bool|String Returns the distinguished name, or false when the given query yields no results + */ public function fetchDN($query, $fields = array()) { $rows = $this->fetchAll($query, $fields); if (count($rows) !== 1) { - throw new \Exception( - sprintf( - 'Cannot fetch single DN for %s', - $query - ) - ); + return false; } return key($rows); } From 6c82cb8988349b513f862a704b0db5d02c0792a7 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 11 Jun 2014 14:22:52 +0200 Subject: [PATCH 09/29] Check ldap backend health during Authentication Check if authentication is possible during authentication, to generate more useful error and log messages, in case the backend configuration is wrong ref #6457 --- .../Backend/LdapUserBackend.php | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index 44445cec8..37216fdd5 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -75,12 +75,46 @@ class LdapUserBackend extends UserBackend ); } + /** + * Probe the backend to test if authentication is possible + * + * Try to bind to the backend and query all available users to check if: + *
    + *
  • User connection credentials are correct and the bind is possible
  • + *
  • At least one user exists
  • + *
  • The specified userClass has the property specified by userNameAttribute
  • + *
+ * + * @throws AuthenticationException When authentication is not possible + */ + protected function assertAuthenticationPossible() + { + $q = $this->conn->select()->from($this->userClass); + $result = $q->fetchRow(); + if (!isset($result)) { + throw new AuthenticationException( + sprintf('No users with objectClass="%s" in DN="%s" available', + $this->userClass, + $this->userNameAttribute + )); + } + + if (!isset($result->{$this->userNameAttribute})) { + throw new AuthenticationException( + sprintf('UserNameAttribute "%s" not existing in objectClass="%s"', + $this->userNameAttribute, + $this->userClass + )); + } + } + /** * Test whether the given user exists * * @param User $user * * @return bool + * @throws AuthenticationException */ public function hasUser(User $user) { @@ -93,12 +127,29 @@ class LdapUserBackend extends UserBackend * * @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 * * @return bool True when the authentication was successful, false when the username or password was invalid - * @throws AuthenticationException When an error occurred during authentication + * @throws AuthenticationException When an error occurred during authentication and authentication is not possible */ - public function authenticate(User $user, $password) + public function authenticate(User $user, $password, $healthCheck = true) { + if ($healthCheck) { + try { + $this->assertAuthenticationPossible(); + } catch (AuthenticationException $e) { + // Authentication not possible + throw new AuthenticationException( + sprintf( + 'Authentication against backend "%s" not possible: ', + $this->getName() + ), + 0, + $e + ); + } + } try { $userDn = $this->conn->fetchDN($this->createQuery($user->getUsername())); if (!$userDn) { @@ -107,6 +158,7 @@ class LdapUserBackend extends UserBackend } return $this->conn->testCredentials($userDn, $password); } catch (Exception $e) { + // Error during authentication of this specific user throw new AuthenticationException( sprintf( 'Failed to authenticate user "%s" against backend "%s". An exception was thrown:', @@ -126,6 +178,7 @@ class LdapUserBackend extends UserBackend */ public function count() { + return $this->conn->count( $this->conn->select()->from( $this->userClass, From c42c7977bef5f8e2bdb844fa7a7739733e963ad1 Mon Sep 17 00:00:00 2001 From: Matthias Jentsch Date: Wed, 11 Jun 2014 15:04:19 +0200 Subject: [PATCH 10/29] Call extended backend health checks when creating ldap authentication backends fixes #6457 --- application/forms/Config/Authentication/LdapBackendForm.php | 3 +++ library/Icinga/Authentication/Backend/LdapUserBackend.php | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/application/forms/Config/Authentication/LdapBackendForm.php b/application/forms/Config/Authentication/LdapBackendForm.php index 966cea1cc..051b3c0c0 100644 --- a/application/forms/Config/Authentication/LdapBackendForm.php +++ b/application/forms/Config/Authentication/LdapBackendForm.php @@ -179,9 +179,12 @@ class LdapBackendForm extends BaseBackendForm $backendConfig->user_class, $backendConfig->user_name_attribute ); + $testConn->assertAuthenticationPossible(); + /* if ($testConn->count() === 0) { throw new Exception('No Users Found On Directory Server'); } + */ } catch (Exception $exc) { $this->addErrorMessage( t('Connection Validation Failed: ' . $exc->getMessage()) diff --git a/library/Icinga/Authentication/Backend/LdapUserBackend.php b/library/Icinga/Authentication/Backend/LdapUserBackend.php index 37216fdd5..d19437ae6 100644 --- a/library/Icinga/Authentication/Backend/LdapUserBackend.php +++ b/library/Icinga/Authentication/Backend/LdapUserBackend.php @@ -87,15 +87,15 @@ class LdapUserBackend extends UserBackend * * @throws AuthenticationException When authentication is not possible */ - protected function assertAuthenticationPossible() + public function assertAuthenticationPossible() { $q = $this->conn->select()->from($this->userClass); $result = $q->fetchRow(); if (!isset($result)) { throw new AuthenticationException( - sprintf('No users with objectClass="%s" in DN="%s" available', + sprintf('No objects with objectClass="%s" in DN="%s" found.', $this->userClass, - $this->userNameAttribute + $this->conn->getDN() )); } From c09341d77ece175be4b7ed8c3499c15f2d6af101 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 11 Jun 2014 14:43:27 +0200 Subject: [PATCH 11/29] Autologin: Do NOT sanitize username I don't know the reason why this was done initially but a username must not be changed. --- library/Icinga/Authentication/Backend/AutoLoginBackend.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/library/Icinga/Authentication/Backend/AutoLoginBackend.php b/library/Icinga/Authentication/Backend/AutoLoginBackend.php index 8fd7a7c5c..173dde6a7 100644 --- a/library/Icinga/Authentication/Backend/AutoLoginBackend.php +++ b/library/Icinga/Authentication/Backend/AutoLoginBackend.php @@ -57,11 +57,7 @@ class AutoLoginBackend extends UserBackend && isset($_SERVER['AUTH_TYPE']) && in_array($_SERVER['AUTH_TYPE'], array('Basic', 'Digest')) === true ) { - $username = filter_var( - $_SERVER['PHP_AUTH_USER'], - FILTER_SANITIZE_STRING, - FILTER_FLAG_ENCODE_HIGH|FILTER_FLAG_ENCODE_LOW - ); + $username = $_SERVER['PHP_AUTH_USER']; if ($username !== false) { if ($this->stripUsernameRegexp !== null) { From 63fc8eb27eed1b96b874146944bac75838bb8fab Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 11 Jun 2014 14:47:15 +0200 Subject: [PATCH 12/29] Autologin: Use REMOTE_USER for authentication It's not safe to rely on PHP_AUTH_USER and PHP_AUTH_TYPE because PHP cgi handlers (fgcid for example) only set the REMOTE_USER environment variable and the authentication type for negogiation methods (Kerberos for example) is neither Basic nor Digest. We may have to add REDIRECT_REMOTE_USER for authentication for proxy setups. --- library/Icinga/Authentication/Backend/AutoLoginBackend.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/library/Icinga/Authentication/Backend/AutoLoginBackend.php b/library/Icinga/Authentication/Backend/AutoLoginBackend.php index 173dde6a7..2fdfe1037 100644 --- a/library/Icinga/Authentication/Backend/AutoLoginBackend.php +++ b/library/Icinga/Authentication/Backend/AutoLoginBackend.php @@ -53,11 +53,8 @@ class AutoLoginBackend extends UserBackend */ public function hasUser(User $user) { - if (isset($_SERVER['PHP_AUTH_USER']) - && isset($_SERVER['AUTH_TYPE']) - && in_array($_SERVER['AUTH_TYPE'], array('Basic', 'Digest')) === true - ) { - $username = $_SERVER['PHP_AUTH_USER']; + if (isset($_SERVER['REMOTE_USER'])) { + $username = $_SERVER['REMOTE_USER']; if ($username !== false) { if ($this->stripUsernameRegexp !== null) { From 7215ba4f595b9136fbd7cd882002c6ed15365832 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 11 Jun 2014 15:04:15 +0200 Subject: [PATCH 13/29] Autologin: Do not require a bogus password in the source code --- application/controllers/AuthenticationController.php | 5 ++--- library/Icinga/Authentication/Backend/AutoLoginBackend.php | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php index f50987de7..4476ecebd 100644 --- a/application/controllers/AuthenticationController.php +++ b/application/controllers/AuthenticationController.php @@ -63,8 +63,6 @@ class AuthenticationController extends ActionController $this->view->form = new LoginForm(); $this->view->form->setRequest($this->_request); $this->view->title = $this->translate('Icingaweb Login'); - $user = new User(''); - $password = ''; try { $redirectUrl = Url::fromPath($this->_request->getParam('redirect', 'dashboard')); @@ -95,9 +93,10 @@ class AuthenticationController extends ActionController if ($this->getRequest()->isGet()) { + $user = new User(''); foreach ($chain as $backend) { if ($backend instanceof AutoLoginBackend) { - $authenticated = $backend->authenticate($user, $password); + $authenticated = $backend->authenticate($user); if ($authenticated === true) { $auth->setAuthenticated($user); $this->redirectNow($redirectUrl); diff --git a/library/Icinga/Authentication/Backend/AutoLoginBackend.php b/library/Icinga/Authentication/Backend/AutoLoginBackend.php index 2fdfe1037..8cde07878 100644 --- a/library/Icinga/Authentication/Backend/AutoLoginBackend.php +++ b/library/Icinga/Authentication/Backend/AutoLoginBackend.php @@ -75,7 +75,7 @@ class AutoLoginBackend extends UserBackend * * @return bool */ - public function authenticate(User $user, $password) + public function authenticate(User $user, $password = null) { return $this->hasUser($user); } From 65a2bd41bce8fa601f6a55d0a0da5b9a00909551 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 11 Jun 2014 15:09:06 +0200 Subject: [PATCH 14/29] Autologin: Do not use absolute `use' --- library/Icinga/Authentication/Backend/AutoLoginBackend.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/Icinga/Authentication/Backend/AutoLoginBackend.php b/library/Icinga/Authentication/Backend/AutoLoginBackend.php index 8cde07878..5a72f475a 100644 --- a/library/Icinga/Authentication/Backend/AutoLoginBackend.php +++ b/library/Icinga/Authentication/Backend/AutoLoginBackend.php @@ -4,9 +4,9 @@ namespace Icinga\Authentication\Backend; +use Zend_Config; use Icinga\Authentication\UserBackend; use Icinga\User; -use \Zend_Config; /** * Test login with external authentication mechanism, e.g. Apache From 992ccf4f6dfe3d422474f810cde7b1d18528d185 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 11 Jun 2014 15:27:36 +0200 Subject: [PATCH 15/29] Autologin: Actually set the username upon authentication Before, when using autologin the username of the authenticated user always was the empty string. --- .../Authentication/Backend/AutoLoginBackend.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/library/Icinga/Authentication/Backend/AutoLoginBackend.php b/library/Icinga/Authentication/Backend/AutoLoginBackend.php index 5a72f475a..67206b8de 100644 --- a/library/Icinga/Authentication/Backend/AutoLoginBackend.php +++ b/library/Icinga/Authentication/Backend/AutoLoginBackend.php @@ -55,13 +55,16 @@ class AutoLoginBackend extends UserBackend { if (isset($_SERVER['REMOTE_USER'])) { $username = $_SERVER['REMOTE_USER']; - - if ($username !== false) { - if ($this->stripUsernameRegexp !== null) { - $username = preg_replace($this->stripUsernameRegexp, '', $username); + if ($this->stripUsernameRegexp !== null) { + $stripped = preg_replace($this->stripUsernameRegexp, '', $username); + if ($stripped !== false) { + // TODO(el): PHP issues a warning when PHP cannot compile the regular expression. Should we log an + // additional message in that case? + $username = $stripped; } - return true; } + $user->setUsername($username); + return true; } return false; From 7d2ee41f4247ee1340e7d9044399e8af3eb70a29 Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Wed, 11 Jun 2014 15:33:33 +0200 Subject: [PATCH 16/29] Autologin: Fix PHPDoc --- .../Authentication/Backend/AutoLoginBackend.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/library/Icinga/Authentication/Backend/AutoLoginBackend.php b/library/Icinga/Authentication/Backend/AutoLoginBackend.php index 67206b8de..d793b50dd 100644 --- a/library/Icinga/Authentication/Backend/AutoLoginBackend.php +++ b/library/Icinga/Authentication/Backend/AutoLoginBackend.php @@ -31,13 +31,11 @@ class AutoLoginBackend extends UserBackend } /** - * (PHP 5 >= 5.1.0)
- * Count elements of an object - * @link http://php.net/manual/en/countable.count.php - * @return int The custom count as an integer. - *

- *

- * The return value is cast to an integer. + * Count the available users + * + * Autologin backends will always return 1 + * + * @return int */ public function count() { @@ -73,8 +71,8 @@ class AutoLoginBackend extends UserBackend /** * Authenticate * - * @param User $user - * @param string $password + * @param User $user + * @param string $password * * @return bool */ From dfcf3d28e686af99d3685e8a914df003b6ad925d Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 12 Jun 2014 08:07:04 +0000 Subject: [PATCH 17/29] CSS/pagination: avoid text-selection Clicking fast through pagination resulted in irritating text selections and therefore uncomfortable behaviour. Should be fixed now. --- public/css/icinga/pagination.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/css/icinga/pagination.less b/public/css/icinga/pagination.less index 21336d145..899f34a2f 100644 --- a/public/css/icinga/pagination.less +++ b/public/css/icinga/pagination.less @@ -2,6 +2,12 @@ ul.pagination { font-size: 0.68em; padding: 0; display: inline; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } #layout.twocols u.pagination { From 3047992ab576d4caa58c3f4a5330dceb7f7dd675 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 12 Jun 2014 08:20:57 +0000 Subject: [PATCH 18/29] CSS/tables: improve row hover styles Added a default hover color, fix inheritage and missing states. --- public/css/icinga/monitoring-colors.less | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/public/css/icinga/monitoring-colors.less b/public/css/icinga/monitoring-colors.less index d0bc87cc3..0e16a5253 100644 --- a/public/css/icinga/monitoring-colors.less +++ b/public/css/icinga/monitoring-colors.less @@ -127,15 +127,12 @@ tr.state.handled td.state, tr.state.ok td.state, tr.state.up td.state, tr.state. color: black; background-color: transparent; } + tr[href].active { background-color: #ddd; color: black; } -tr.state[href]:hover, tr.state[href].active td.state { - -} - tr.state.ok td.state, tr.state.up td.state { border-left-color: @colorOk; } @@ -195,12 +192,12 @@ tr.state.handled td.state { /* HOVER colors */ - -tr.state[href]:hover, tr.state[href]:hover td.state { +tr[href]:hover, tr.state[href]:hover td.state { color: white; + background-color: #555; } -tr.state.ok:hover { +tr.state.ok[href]:hover, tr.state.up[href]:hover { background-color: @colorOk; } @@ -248,6 +245,11 @@ tr.state.unreachable[href]:hover { tr.state.unreachable.handled[href]:hover { background-color: @colorUnreachableHandled; } + +tr.state[href]:hover td.state { + background-color: inherit !important; +} + /* END of HOVER colors */ /* END of special tables and states */ From d28d20696c23cec75e63b2a3eec7025e590ecb1e Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 12 Jun 2014 14:16:28 +0200 Subject: [PATCH 19/29] Move binary testing stuff and phpunit.xml to its own module refs #6092 --- {test/php => modules/test}/Makefile | 0 {test/php => modules/test}/bin/README | 0 {test/php => modules/test}/bin/common.h | 0 {test/php => modules/test}/bin/extcmd_test.c | 0 {test/php => modules/test}/bin/shared.h | 0 {test/php => modules/test}/phpunit.xml | 15 +++++++-------- 6 files changed, 7 insertions(+), 8 deletions(-) rename {test/php => modules/test}/Makefile (100%) rename {test/php => modules/test}/bin/README (100%) rename {test/php => modules/test}/bin/common.h (100%) rename {test/php => modules/test}/bin/extcmd_test.c (100%) rename {test/php => modules/test}/bin/shared.h (100%) rename {test/php => modules/test}/phpunit.xml (72%) diff --git a/test/php/Makefile b/modules/test/Makefile similarity index 100% rename from test/php/Makefile rename to modules/test/Makefile diff --git a/test/php/bin/README b/modules/test/bin/README similarity index 100% rename from test/php/bin/README rename to modules/test/bin/README diff --git a/test/php/bin/common.h b/modules/test/bin/common.h similarity index 100% rename from test/php/bin/common.h rename to modules/test/bin/common.h diff --git a/test/php/bin/extcmd_test.c b/modules/test/bin/extcmd_test.c similarity index 100% rename from test/php/bin/extcmd_test.c rename to modules/test/bin/extcmd_test.c diff --git a/test/php/bin/shared.h b/modules/test/bin/shared.h similarity index 100% rename from test/php/bin/shared.h rename to modules/test/bin/shared.h diff --git a/test/php/phpunit.xml b/modules/test/phpunit.xml similarity index 72% rename from test/php/phpunit.xml rename to modules/test/phpunit.xml index b36e81fe8..83fc9d9ac 100644 --- a/test/php/phpunit.xml +++ b/modules/test/phpunit.xml @@ -1,25 +1,24 @@ - + - application/ - bin/ - library/ + ../../test/php/application/ + ../../test/php/library/ - ../../modules/*/test/php - ../../modules/*/test/php/regression + ../*/test/php + ../*/test/php/regression - regression/ - ../../modules/*/test/regression + ../../test/php/regression/ + ../*/test/php/regression From 32a7decc3e4ee153d177a7a7545d1ee10c95ffa7 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 12 Jun 2014 14:16:53 +0200 Subject: [PATCH 20/29] Remove python test-runners refs #6092 --- test/php/checkswag | 127 --------------------------------------------- test/php/runtests | 124 ------------------------------------------- 2 files changed, 251 deletions(-) delete mode 100755 test/php/checkswag delete mode 100755 test/php/runtests diff --git a/test/php/checkswag b/test/php/checkswag deleted file mode 100755 index c5abdb5d6..000000000 --- a/test/php/checkswag +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import subprocess -from pipes import quote -from optparse import OptionParser, BadOptionError, AmbiguousOptionError - - -APPLICATION = 'phpcs' -DEFAULT_ARGS = ['-p', '--standard=PSR2', '--extensions=php', - '--encoding=utf-8'] - -VAGRANT_SCRIPT = '/vagrant/test/php/checkswag' -REPORT_DIRECTORY = '../../build/log' - - -class PassThroughOptionParser(OptionParser): - """ - An unknown option pass-through implementation of OptionParser. - - When unknown arguments are encountered, bundle with largs and try again, - until rargs is depleted. - - sys.exit(status) will still be called if a known argument is passed - incorrectly (e.g. missing arguments or bad argument types, etc.) - - Borrowed from: http://stackoverflow.com/a/9307174 - """ - def _process_args(self, largs, rargs, values): - while rargs: - try: - OptionParser._process_args(self, largs, rargs, values) - except (BadOptionError, AmbiguousOptionError), error: - largs.append(error.opt_str) - - -def execute_command(command, return_output=False, shell=False): - prog = subprocess.Popen(command, shell=shell, - stdout=subprocess.PIPE - if return_output - else None) - return prog.wait() if not return_output else \ - prog.communicate()[0] - - -def get_report_directory(): - path = os.path.abspath(REPORT_DIRECTORY) - - try: - os.makedirs(REPORT_DIRECTORY) - except OSError: - pass - - return path - - -def get_script_directory(): - return os.path.dirname(os.path.abspath(sys.argv[0])) - - -def parse_commandline(): - parser = PassThroughOptionParser(usage='%prog [options] [additional arguments' - ' for {0}]'.format(APPLICATION)) - parser.add_option('-b', '--build', action='store_true', - help='Enable reporting.') - parser.add_option('-v', '--verbose', action='store_true', - help='Be more verbose.') - parser.add_option('-i', '--include', metavar='PATTERN', action='append', - help='Include only specific files/test cases.' - ' (Can be supplied multiple times.)') - parser.add_option('-e', '--exclude', metavar='PATTERN', action='append', - help='Exclude specific files/test cases. ' - '(Can be supplied multiple times.)') - parser.add_option('-V', '--vagrant', action='store_true', - help='Run in vagrant VM') - return parser.parse_args() - - -def main(): - options, arguments = parse_commandline() - - if options.vagrant and os.environ['USER'] != 'vagrant': - # Check if vagrant is installed - vagrant_path = execute_command('which vagrant', True, True).strip() - if not vagrant_path: - print 'ERROR: vagrant not found!' - return 2 - - # Call the script in the Vagrant VM with the same parameters - commandline = ' '.join(quote(p) for p in sys.argv[1:]) - return execute_command('vagrant ssh -c "{0} {1}"' - ''.format(VAGRANT_SCRIPT, commandline), - shell=True) - else: - # Environment preparation and verification - os.chdir(get_script_directory()) - application_path = execute_command('which {0}'.format(APPLICATION), - True, True).strip() - if not application_path: - print 'ERROR: {0} not found!'.format(APPLICATION) - return 2 - - # Commandline preparation - command_options = [] - if options.verbose: - command_options.append('-v') - if options.build: - result_path = os.path.join(get_report_directory(), - 'phpcs_results.xml') - command_options.append('--report-checkstyle=' + result_path) - if options.exclude: - command_options.append('--ignore=' + ','.join(options.exclude)) - if options.include: - arguments.extend(options.include) - else: - arguments.extend(['../../application', '../../bin', - '../../library/Icinga']) - - # Application invocation.. - execute_command([application_path] + DEFAULT_ARGS + - command_options + arguments) - return 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/test/php/runtests b/test/php/runtests deleted file mode 100755 index 13f53db1f..000000000 --- a/test/php/runtests +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python - -import os -import sys -import subprocess -from pipes import quote -from fnmatch import fnmatch -from optparse import OptionParser, BadOptionError, AmbiguousOptionError - - -APPLICATION = 'phpunit' -DEFAULT_ARGS = [] - -VAGRANT_SCRIPT = '/vagrant/test/php/runtests' -REPORT_DIRECTORY = '../../build/log' - - -class PassThroughOptionParser(OptionParser): - """ - An unknown option pass-through implementation of OptionParser. - - When unknown arguments are encountered, bundle with largs and try again, - until rargs is depleted. - - sys.exit(status) will still be called if a known argument is passed - incorrectly (e.g. missing arguments or bad argument types, etc.) - - Borrowed from: http://stackoverflow.com/a/9307174 - """ - def _process_args(self, largs, rargs, values): - while rargs: - try: - OptionParser._process_args(self, largs, rargs, values) - except (BadOptionError, AmbiguousOptionError), error: - largs.append(error.opt_str) - - -def execute_command(command, return_output=False, shell=False): - prog = subprocess.Popen(command, shell=shell, - stdout=subprocess.PIPE - if return_output - else None) - return prog.wait() if not return_output else \ - prog.communicate()[0] - - -def get_report_directory(): - path = os.path.abspath(REPORT_DIRECTORY) - - try: - os.makedirs(REPORT_DIRECTORY) - except OSError: - pass - - return path - - -def get_script_directory(): - return os.path.dirname(os.path.abspath(sys.argv[0])) - - -def parse_commandline(): - parser = PassThroughOptionParser(usage='%prog [options] [additional arguments' - ' for {0}]'.format(APPLICATION)) - parser.add_option('-b', '--build', action='store_true', - help='Enable reporting.') - parser.add_option('-v', '--verbose', action='store_true', - help='Be more verbose.') - parser.add_option('-i', '--include', metavar='PATTERN', - help='Include only specific files/test cases.') - parser.add_option('-V', '--vagrant', action='store_true', - help='Run in vagrant VM') - return parser.parse_args() - - -def main(): - options, arguments = parse_commandline() - - if options.vagrant and os.environ['USER'] != 'vagrant': - # Check if vagrant is installed - vagrant_path = execute_command('which vagrant', True, True).strip() - if not vagrant_path: - print 'ERROR: vagrant not found!' - return 2 - - # Call the script in the Vagrant VM with the same parameters - commandline = ' '.join(quote(p) for p in sys.argv[1:]) - return execute_command('vagrant ssh -c "{0} {1}"' - ''.format(VAGRANT_SCRIPT, commandline), - shell=True) - else: - # Environment preparation and verification - os.chdir(get_script_directory()) - application_path = execute_command('which {0}'.format(APPLICATION), - True, True).strip() - if not application_path: - print 'ERROR: {0} not found!'.format(APPLICATION) - return 2 - if not os.path.isfile('./bin/extcmd_test'): - execute_command('make', shell=True) - - # Commandline preparation - command_options = [] - if options.verbose: - command_options.append('--verbose') - if options.build: - report_directory = get_report_directory() - command_options.append('--log-junit') - command_options.append(os.path.join(report_directory, - 'phpunit_results.xml')) - command_options.append('--coverage-html') - command_options.append(os.path.join(report_directory, - 'php_html_coverage')) - if options.include: - command_options.append('--filter') - command_options.append(options.include) - - # Application invocation.. - execute_command([application_path] + DEFAULT_ARGS + - command_options + arguments) - return 0 - -if __name__ == '__main__': - sys.exit(main()) From 0805d73e34d3692ffb77a74c31bd04fbdb67ba27 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 12 Jun 2014 16:07:25 +0200 Subject: [PATCH 21/29] Add clicommands to run unit- and style-tests refs #6092 --- library/Icinga/Cli/Params.php | 24 +- library/Icinga/Util/Process.php | 205 ++++++++++++++++++ .../application/clicommands/PhpCommand.php | 163 ++++++++++++++ 3 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 library/Icinga/Util/Process.php create mode 100644 modules/test/application/clicommands/PhpCommand.php diff --git a/library/Icinga/Cli/Params.php b/library/Icinga/Cli/Params.php index 1fc47a150..63ae06ea7 100644 --- a/library/Icinga/Cli/Params.php +++ b/library/Icinga/Cli/Params.php @@ -10,12 +10,20 @@ class Params public function __construct($argv) { + $noOptionFlag = false; $this->program = array_shift($argv); for ($i = 0; $i < count($argv); $i++) { - if (substr($argv[$i], 0, 2) === '--') { + if ($argv[$i] === '--') { + $noOptionFlag = true; + } elseif (!$noOptionFlag && substr($argv[$i], 0, 2) === '--') { $key = substr($argv[$i], 2); if (! isset($argv[$i + 1]) || substr($argv[$i + 1], 0, 2) === '--') { $this->params[$key] = true; + } elseif (array_key_exists($key, $this->params)) { + if (!is_array($this->params[$key])) { + $this->params[$key] = array($this->params[$key]); + } + $this->params[$key][] = $argv[++$i]; } else { $this->params[$key] = $argv[++$i]; } @@ -43,6 +51,11 @@ class Params return $this->params; } + public function getAllStandalone() + { + return $this->standalone; + } + public function __get($key) { return $this->get($key); @@ -95,7 +108,14 @@ class Params return $default; } $result = $this->get($key, $default); - $this->remove($key); + if (is_array($result) && !is_array($default)) { + $result = array_shift($result) || $default; + if ($result === $default) { + $this->remove($key); + } + } else { + $this->remove($key); + } return $result; } diff --git a/library/Icinga/Util/Process.php b/library/Icinga/Util/Process.php new file mode 100644 index 000000000..b3d2b9fe4 --- /dev/null +++ b/library/Icinga/Util/Process.php @@ -0,0 +1,205 @@ +resource = proc_open( + $cmd, + $descriptorSpec, + $this->pipes, + $cwd, + $env + ); + + if (!is_resource($this->resource)) { + throw new RuntimeException("Cannot start process: $cmd"); + } + } + + /** + * Start and return a new process + * + * @param string $cmd The command to start the process + * @param string $cwd The working directory of the new process (Must be an absolute path or null) + * @param string $stdout A filedescriptor, "pipe" or a filepath + * @param string $stderr A filedescriptor, "pipe" or a filepath + * @param string $stdin A filedescriptor, "pipe" or a filepath + * @param array $env The environment variables (Must be an array or null) + * + * @return Process + * + * @throws RuntimeException When the process could not be started + */ + public static function start($cmd, $cwd = null, $stdout = null, $stderr = null, $stdin = null, $env = array()) + { + return new static($cmd, $cwd, $stdout, $stderr, $stdin, $env); + } + + /** + * Interact with process + * + * Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. + * Wait for process to terminate. The optional input argument should be a string to be + * sent to the child process, or null, if no data should be sent to the child. + * + * Note that you need to pass the equivalent pipes to the constructor for this to work. + * + * @param string $input Data to send to the child. + * + * @return array The data from stdout and stderr. + */ + public function communicate($input = null) + { + if (!isset($this->pipes[1]) && !isset($this->pipes[2])) { + $this->wait(); + return array(); + } + + $read = $write = array(); + if (isset($this->pipes[0])) { + $write[] = $this->pipes[0]; + } + if (isset($this->pipes[1])) { + $read[] = $this->pipes[1]; + stream_set_blocking($this->pipes[1], 0); + } + if (isset($this->pipes[2])) { + $read[] = $this->pipes[2]; + stream_set_blocking($this->pipes[2], 0); + } + + $stdout = $stderr = ''; + $readToWatch = $read; + $writeToWatch = $write; + $exceptToWatch = array(); + while (stream_select($readToWatch, $writeToWatch, $exceptToWatch, 0, 20000) !== false) { + if (!empty($writeToWatch) && $input) { + $input = substr($input, fwrite($writeToWatch[0], $input)); + } + foreach ($readToWatch as $pipe) { + if (isset($this->pipes[1]) && $pipe === $this->pipes[1]) { + $stdout .= stream_get_contents($pipe); + } elseif (isset($this->pipes[2]) && $pipe === $this->pipes[2]) { + $stderr .= stream_get_contents($pipe); + } + } + + $readToWatch = array_filter($read, function ($h) { return !feof($h); }); + $writeToWatch = $input ? $write : array(); + if (empty($readToWatch) && empty($writeToWatch)) { + break; + } + } + + $this->wait(); // To ensure the process is actually stopped when calling cleanUp() we utilize wait() + return array($stdout, $stderr); + } + + /** + * Return whether the process is still alive and set the returncode + * + * @return bool + */ + public function poll() + { + if ($this->resource !== null) { + $info = @proc_get_status($this->resource); + if ($info !== false) { + if ($info['running']) { + return true; + } elseif ($info['exitcode'] !== -1) { + $this->returnCode = $info['exitcode']; + } + } + } + + return false; + } + + /** + * Wait for process to terminate and return its returncode + * + * @return int + */ + public function wait() + { + if ($this->returnCode === null && $this->resource !== null) { + while ($this->poll()) { + usleep(500000); + } + $this->cleanUp(); + } + + return $this->returnCode; + } + + /** + * Cleanup the process resource and its associated pipes + */ + protected function cleanUp() + { + foreach ($this->pipes as $pipe) { + if (is_resource($pipe)) { + fclose($pipe); + } + } + + proc_close($this->resource); + $this->resource = null; + } +} diff --git a/modules/test/application/clicommands/PhpCommand.php b/modules/test/application/clicommands/PhpCommand.php new file mode 100644 index 000000000..f5a4b49fe --- /dev/null +++ b/modules/test/application/clicommands/PhpCommand.php @@ -0,0 +1,163 @@ +params->shift('build'); + $include = $this->params->shift('include'); + + $phpUnit = exec('which phpunit'); + if (!file_exists($phpUnit)) { + $this->fail('PHPUnit not found. Please install PHPUnit to be able to run the unit-test suites.'); + } + + $options = array(); + if ($this->isVerbose) { + $options[] = '--verbose'; + } + if ($build) { + $reportPath = $this->setupAndReturnReportDirectory(); + echo $reportPath; + $options[] = '--log-junit'; + $options[] = $reportPath . '/phpunit_results.xml'; + $options[] = '--coverage-html'; + $options[] = $reportPath . '/php_html_coverage'; + } + if ($include !== null) { + $options[] = '--filter'; + $options[] = $include; + } + + Process::start( + $phpUnit . ' ' . join(' ', array_merge($options, $this->params->getAllStandalone())), + realpath(__DIR__ . '/../..') + )->wait(); + } + + /** + * Run code-style checks + * + * This command checks whether icingaweb and installed modules match the PSR-2 coding standard. + * + * USAGE + * + * icingacli test php style [options] + * + * OPTIONS + * + * --verbose Be more verbose. + * --build Enable reporting. + * --include Include only specific files. (Can be supplied multiple times.) + * --exclude Pattern to use for excluding files. (Can be supplied multiple times.) + * + * EXAMPLES + * + * icingacli test php style --verbose + * icingacli test php style --build + * icingacli test php style --include path/to/your/file + * icingacli test php style --exclude *someFile* --exclude someOtherFile* + */ + public function styleAction() + { + $build = $this->params->shift('build'); + $include = (array) $this->params->shift('include', array()); + $exclude = (array) $this->params->shift('exclude', array()); + + $phpcs = exec('which phpcs'); + if (!file_exists($phpcs)) { + $this->fail( + 'PHP_CodeSniffer not found. Please install PHP_CodeSniffer to be able to run code style tests.' + ); + } + + $options = array(); + if ($this->isVerbose) { + $options[] = '-v'; + } + if ($build) { + $options[] = '--report-checkstyle=' . $this->setupAndReturnReportDirectory(); + } + if (!empty($exclude)) { + $options[] = '--ignore=' . join(',', $exclude); + } + $arguments = array_filter(array_map(function ($p) { return realpath($p); }, $include)); + if (empty($arguments)) { + $arguments = array( + realpath(__DIR__ . '/../../../../application'), + realpath(__DIR__ . '/../../../../library/Icinga') + ); + } + + Process::start( + $phpcs . ' ' . join( + ' ', + array_merge( + $options, + $this->phpcsDefaultParams, + $arguments, + $this->params->getAllStandalone() + ) + ), + realpath(__DIR__ . '/../..') + )->wait(); + } + + /** + * Setup the directory where to put report files and return its path + * + * @return string + */ + protected function setupAndReturnReportDirectory() + { + $path = realpath(__DIR__ . '/../../../..') . '/build/log'; + if (!file_exists($path) && !@mkdir($path, 0755, true)) { + $this->fail("Could not create directory: $path"); + } + + return $path; + } +} From db73d324deda52ee04dd442fbde73f6abc1839db Mon Sep 17 00:00:00 2001 From: Eric Lippmann Date: Thu, 12 Jun 2014 17:05:54 +0200 Subject: [PATCH 22/29] Autologin: Fix that the backend name must have been `autologin' Before, the code validated the name of the backend instead of the `backend' directive against `autologin'. --- library/Icinga/Authentication/UserBackend.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/library/Icinga/Authentication/UserBackend.php b/library/Icinga/Authentication/UserBackend.php index 70070aa2f..9dcf9c641 100644 --- a/library/Icinga/Authentication/UserBackend.php +++ b/library/Icinga/Authentication/UserBackend.php @@ -85,7 +85,14 @@ abstract class UserBackend implements Countable } return new $backendConfig->class($backendConfig); } - if ($name === 'autologin') { + if (($backendType = $backendConfig->backend) === null) { + throw new ConfigurationError( + 'Authentication configuration for backend "' . $name + . '" is missing the backend directive' + ); + } + $backendType = strtolower($backendType); + if ($backendType === 'autologin') { $backend = new AutoLoginBackend($backendConfig); $backend->setName($name); return $backend; @@ -96,12 +103,6 @@ abstract class UserBackend implements Countable . '" is missing the resource directive' ); } - if (($backendType = $backendConfig->backend) === null) { - throw new ConfigurationError( - 'Authentication configuration for backend "' . $name - . '" is missing the backend directive' - ); - } try { $resourceConfig = ResourceFactory::getResourceConfig($backendConfig->resource); } catch (ProgrammingError $e) { @@ -110,7 +111,7 @@ abstract class UserBackend implements Countable ); } $resource = ResourceFactory::createResource($resourceConfig); - switch (strtolower($backendType)) { + switch ($backendType) { case 'db': $backend = new DbUserBackend($resource); break; From c3eae116242d7040301088606efb9af34285d04c Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 12 Jun 2014 17:22:17 +0000 Subject: [PATCH 23/29] JS/IE8: fix IE8 error caused by focus() No more error when using the jQuery wrapper. Focus handling is pretty outdated, needs special care as soon as we have auto-refreshing search fields. --- public/js/icinga/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js index 918b890d8..96e7b5a92 100644 --- a/public/js/icinga/loader.js +++ b/public/js/icinga/loader.js @@ -612,7 +612,7 @@ $container.scrollTop(scrollPos); } if (origFocus) { - origFocus.focus(); + $(origFocus).focus(); } // TODO: this.icinga.events.refreshContainer(container); From 0d15f24f05ce3eab26b8b5b55d11335a41a200b8 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 12 Jun 2014 17:25:48 +0000 Subject: [PATCH 24/29] JS/IE: remove unused conditional comments There is no IE7 support and no special handling for IE9, therefore I removed unused conditional comments. Left IE8 there as it might ask for special CSS - however it doesn't right now. Also added missing iframe-class for Iframes on IE8. --- application/layouts/scripts/layout.phtml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml index 16a245a15..602a0f0c7 100644 --- a/application/layouts/scripts/layout.phtml +++ b/application/layouts/scripts/layout.phtml @@ -11,16 +11,13 @@ if (array_key_exists('_dev', $_GET)) { } $isIframe = isset($_GET['_render']) && $_GET['_render'] === 'iframe'; +$iframeClass = $isIframe ? ' iframe' : ''; ?> - - + - + From 44a7aa6adbace02a74f13ba38e94185c28ce2028 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Thu, 12 Jun 2014 17:33:28 +0000 Subject: [PATCH 25/29] JS/IE8: deliver legacy jQuery for IE8 This patch makes IcingaWeb deliver a legacy jQuery version for IE8 as it is no longer supported in jQuery 2.x. JS for IE8 will not be delivered minified to ease troubleshooting on that buggy platform. fixes #5866 refs #6417 --- application/layouts/scripts/layout.phtml | 7 +++++++ library/Icinga/Application/webrouter.php | 5 +++++ library/Icinga/Web/JavaScript.php | 12 +++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml index 602a0f0c7..71b233610 100644 --- a/application/layouts/scripts/layout.phtml +++ b/application/layouts/scripts/layout.phtml @@ -10,6 +10,8 @@ if (array_key_exists('_dev', $_GET)) { $cssfile = 'css/icinga.min.css'; } +$ie8jsfile = 'js/icinga.ie8.js'; + $isIframe = isset($_GET['_render']) && $_GET['_render'] === 'iframe'; $iframeClass = $isIframe ? ' iframe' : ''; @@ -47,7 +49,12 @@ $iframeClass = $isIframe ? ' iframe' : '';

render('body.phtml') ?>
+ + +