diff --git a/library/Icinga/Authentication/User/LdapUserBackend.php b/library/Icinga/Authentication/User/LdapUserBackend.php index 948dcbe39..c6efd0673 100644 --- a/library/Icinga/Authentication/User/LdapUserBackend.php +++ b/library/Icinga/Authentication/User/LdapUserBackend.php @@ -218,7 +218,7 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface throw new ProgrammingError('It is required to set a attribute name where to find a user\'s name first'); } - if ($this->ds->getCapabilities()->hasAdOid()) { + if ($this->ds->getCapabilities()->isActiveDirectory()) { $isActiveAttribute = 'userAccountControl'; $createdAtAttribute = 'whenCreated'; $lastModifiedAttribute = 'whenChanged'; @@ -254,7 +254,7 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface throw new ProgrammingError('It is required to set the objectClass where to look for users first'); } - if ($this->ds->getCapabilities()->hasAdOid()) { + if ($this->ds->getCapabilities()->isActiveDirectory()) { $stateConverter = 'user_account_control'; } else { $stateConverter = 'shadow_expire'; diff --git a/library/Icinga/Data/Inspectable.php b/library/Icinga/Data/Inspectable.php new file mode 100644 index 000000000..1e037113a --- /dev/null +++ b/library/Icinga/Data/Inspectable.php @@ -0,0 +1,26 @@ +connection->getCapabilities()->hasAdOid(); + return $this->connection->getCapabilities()->isActiveDirectory(); } /** diff --git a/library/Icinga/Protocol/Ldap/Capability.php b/library/Icinga/Protocol/Ldap/LdapCapabilities.php similarity index 59% rename from library/Icinga/Protocol/Ldap/Capability.php rename to library/Icinga/Protocol/Ldap/LdapCapabilities.php index e467884d3..e98c57818 100644 --- a/library/Icinga/Protocol/Ldap/Capability.php +++ b/library/Icinga/Protocol/Ldap/LdapCapabilities.php @@ -9,9 +9,8 @@ namespace Icinga\Protocol\Ldap; * Provides information about the available encryption mechanisms (StartTLS), the supported * LDAP protocol (v2/v3), vendor-specific extensions or protocols controls and extensions. */ -class Capability +class LdapCapabilities { - const LDAP_SERVER_START_TLS_OID = '1.3.6.1.4.1.1466.20037'; const LDAP_PAGED_RESULT_OID_STRING = '1.2.840.113556.1.4.319'; @@ -127,7 +126,7 @@ class Capability } /** - * Return if the capability object contains support for StartTLS + * Return if the capability object contains support for paged results * * @return bool Whether StartTLS is supported */ @@ -141,11 +140,22 @@ class Capability * * @return boolean */ - public function hasAdOid() + public function isActiveDirectory() { return isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_OID]); } + /** + * Whether the ldap server is an OpenLDAP server + * + * @return bool + */ + public function isOpenLdap() + { + return isset($this->attributes->structuralObjectClass) && + $this->attributes->structuralObjectClass === 'OpenLDAProotDSE'; + } + /** * Return if the capability objects contains support for LdapV3, defaults to true if discovery failed * @@ -208,4 +218,120 @@ class Capability } return$this->attributes->namingContexts; } + + public function getVendor() + { + /* + rfc #3045 specifies that the name of the server MAY be included in the attribute 'verndorName', + AD and OpenLDAP don't do this, but for all all other vendors we follow the standard and + just hope for the best. + */ + + if ($this->isActiveDirectory()) { + return 'Microsoft Active Directory'; + } + + if ($this->isOpenLdap()) { + return 'OpenLDAP'; + } + + if (! isset($this->attributes->vendorName)) { + return null; + } + return $this->attributes->vendorName; + } + + public function getVersion() + { + /* + rfc #3045 specifies that the version of the server MAY be included in the attribute 'vendorVersion', + but AD and OpenLDAP don't do this. For OpenLDAP there is no way to query the server versions, but for all + all other vendors we follow the standard and just hope for the best. + */ + + if ($this->isActiveDirectory()) { + return $this->getAdObjectVersionName(); + } + + if (! isset($this->attributes->vendorVersion)) { + return null; + } + return $this->attributes->vendorVersion; + } + + /** + * Discover the capabilities of the given LDAP server + * + * @param LdapConnection $connection The ldap connection to use + * @param int $ds The link identifier of the current LDAP connection + * + * @return LdapCapabilities + * + * @throws LdapException In case the capability query has failed + */ + public static function discoverCapabilities(LdapConnection $connection, $ds) + { + $fields = array( + 'defaultNamingContext', + 'namingContexts', + 'vendorName', + 'vendorVersion', + 'supportedSaslMechanisms', + 'dnsHostName', + 'schemaNamingContext', + 'supportedLDAPVersion', // => array(3, 2) + 'supportedCapabilities', + 'supportedControl', + 'supportedExtension', + 'objectVersion', + '+' + ); + + $result = @ldap_read($ds, '', (string) $connection->select()->from('*', $fields), $fields); + if (! $result) { + throw new LdapException( + 'Capability query failed (%s:%d): %s. Check if hostname and port of the' + . ' ldap resource are correct and if anonymous access is permitted.', + $connection->getHostname(), + $connection->getPort(), + ldap_error($ds) + ); + } + + $entry = ldap_first_entry($ds, $result); + if ($entry === false) { + throw new LdapException( + 'Capabilities not available (%s:%d): %s. Discovery of root DSE probably not permitted.', + $connection->getHostname(), + $connection->getPort(), + ldap_error($ds) + ); + } + $cap = new LdapCapabilities( + $connection->cleanupAttributes( + ldap_get_attributes($ds, $entry), + array_flip($fields) + ) + ); + return $cap; + } + + /** + * Determine the active directory version using the available capabillities + * + * @return null|string The server version description or null when unknown + */ + protected function getAdObjectVersionName() + { + if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_W8_OID])) { + return 'Windows Server 2012 (or newer)'; + } + if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_V61_R2_OID])) { + return 'Windows Server 2008 R2 (or newer)'; + } + if (isset($this->oids[self::LDAP_CAP_ACTIVE_DIRECTORY_V60_OID])) { + return 'Windows Server 2008 (or newer)'; + } + return null; + } } diff --git a/library/Icinga/Protocol/Ldap/LdapConnection.php b/library/Icinga/Protocol/Ldap/LdapConnection.php index 3661e160d..382b61e2d 100644 --- a/library/Icinga/Protocol/Ldap/LdapConnection.php +++ b/library/Icinga/Protocol/Ldap/LdapConnection.php @@ -3,11 +3,13 @@ namespace Icinga\Protocol\Ldap; +use Exception; use ArrayIterator; use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Application\Platform; use Icinga\Data\ConfigObject; +use Icinga\Data\Inspectable; use Icinga\Data\Selectable; use Icinga\Data\Sortable; use Icinga\Exception\ProgrammingError; @@ -16,7 +18,7 @@ use Icinga\Protocol\Ldap\LdapException; /** * Encapsulate LDAP connections and query creation */ -class LdapConnection implements Selectable +class LdapConnection implements Selectable, Inspectable { /** * Indicates that the target object cannot be found @@ -142,7 +144,7 @@ class LdapConnection implements Selectable /** * The properties and capabilities of the LDAP server * - * @var Capability + * @var LdapCapabilities */ protected $capabilities; @@ -160,6 +162,16 @@ class LdapConnection implements Selectable */ protected $encryptionSuccess; + /** + * @var array + */ + protected $info = null; + + /** + * @var Boolean + */ + protected $healthy = null; + /** * Create a new connection object * @@ -243,7 +255,7 @@ class LdapConnection implements Selectable /** * Return the capabilities of the current connection * - * @return Capability + * @return LdapCapabilities */ public function getCapabilities() { @@ -254,7 +266,7 @@ class LdapConnection implements Selectable } catch (LdapException $e) { Logger::debug($e); Logger::warning('LADP discovery failed, assuming default LDAP capabilities.'); - $this->capabilities = new Capability(); // create empty default capabilities + $this->capabilities = new LdapCapabilities(); // create empty default capabilities $this->discoverySuccess = false; } } @@ -385,8 +397,7 @@ class LdapConnection implements Selectable { $this->bind(); - if ( - $query->getUsePagedResults() + if ($query->getUsePagedResults() && version_compare(PHP_VERSION, '5.4.0') >= 0 && $this->getCapabilities()->hasPagedResult() ) { @@ -660,7 +671,7 @@ class LdapConnection implements Selectable if ($serverSorting && $query->hasOrder()) { ldap_set_option($ds, LDAP_OPT_SERVER_CONTROLS, array( array( - 'oid' => Capability::LDAP_SERVER_SORT_OID, + 'oid' => LdapCapabilities::LDAP_SERVER_SORT_OID, 'value' => $this->encodeSortRules($query->getOrder()) ) )); @@ -702,7 +713,8 @@ class LdapConnection implements Selectable $count += 1; if (! $serverSorting || $offset === 0 || $offset < $count) { $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( - ldap_get_attributes($ds, $entry), array_flip($fields) + ldap_get_attributes($ds, $entry), + array_flip($fields) ); } } while ( @@ -755,7 +767,7 @@ class LdapConnection implements Selectable if ($serverSorting && $query->hasOrder()) { ldap_set_option($ds, LDAP_OPT_SERVER_CONTROLS, array( array( - 'oid' => Capability::LDAP_SERVER_SORT_OID, + 'oid' => LdapCapabilities::LDAP_SERVER_SORT_OID, 'value' => $this->encodeSortRules($query->getOrder()) ) )); @@ -814,7 +826,8 @@ class LdapConnection implements Selectable $count += 1; if (! $serverSorting || $offset === 0 || $offset < $count) { $entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes( - ldap_get_attributes($ds, $entry), array_flip($fields) + ldap_get_attributes($ds, $entry), + array_flip($fields) ); } } while ( @@ -870,7 +883,7 @@ class LdapConnection implements Selectable * * @return object */ - protected function cleanupAttributes($attributes, array $requestedFields) + public function cleanupAttributes($attributes, array $requestedFields) { // In case the result contains attributes with a differing case than the requested fields, it is // necessary to create another array to map attributes case insensitively to their requested counterparts. @@ -920,6 +933,7 @@ class LdapConnection implements Selectable * @param array $sortRules * * @return string + * @throws ProgrammingError * * @todo Produces an invalid stream, obviously */ @@ -957,6 +971,10 @@ class LdapConnection implements Selectable $hostname = $this->hostname; if ($this->encryption === static::LDAPS) { + $this->logInfo('Connect using LDAPS'); + if (! $this->validateCertificate) { + $this->logInfo('Skipping certificate validation'); + } $hostname = 'ldaps://' . $hostname; } @@ -971,24 +989,75 @@ class LdapConnection implements Selectable ldap_set_option($ds, LDAP_OPT_REFERRALS, 0); if ($this->encryption === static::STARTTLS) { - if (($this->encryptionSuccess = @ldap_start_tls($ds))) { - Logger::debug('LDAP STARTTLS succeeded'); - } else { - Logger::error('LDAP STARTTLS failed: %s', ldap_error($ds)); - - // ldap_start_tls seems to corrupt the connection though if I understand - // https://tools.ietf.org/html/rfc4511#section-4.14.2 correctly, this shouldn't happen - $ds = ldap_connect($hostname, $this->port); - ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); - ldap_set_option($ds, LDAP_OPT_REFERRALS, 0); + $this->encrypted = true; + $this->logInfo('Connect using STARTTLS'); + if (! $this->validateCertificate) { + $this->logInfo('Skipping certificate validation'); } - } elseif ($this->encryption === static::LDAPS) { - $this->encryptionSuccess = true; + + $ret = ldap_start_tls($ds); + var_dump($ret); + if ($ret) { + } else { + throw new LdapException('LDAP STARTTLS failed: %s', ldap_error($ds)); + } + + } elseif ($this->encryption !== static::LDAPS) { + $this->encrypted = false; + $this->logInfo('Connect without encryption'); } return $ds; } + /** + * Test if needed aspects of the LDAP connection are working as expected + * + * Extended information about the + * + * @throws \Icinga\Protocol\Ldap\LdapException When a critical aspect of the health test fails + */ + public function testConnectionHealth() + { + $this->healthy = false; + $this->info = array(); + + // Try to connect to the server with the given connection parameters + $ds = $this->prepareNewConnection(); + + // Try a bind-command with the given user credentials, this must not fail + $success = @ldap_bind($ds, $this->bindDn, $this->bindPw); + $msg = sprintf( + 'LDAP bind to %s:%s (%s / %s)', + $this->hostname, + $this->port, + $this->bindDn, + '***' /* $this->bindPw */ + ); + if (! $success) { + throw new LdapException('%s failed: %s', $msg, ldap_error($ds)); + } + $this->logInfo(sprintf($msg . ' successful')); + + // Try to execute a schema discovery, this may fail if schema discovery is not supported + try { + $cap = LdapCapabilities::discoverCapabilities($this, $ds); + $infos []= $cap->getVendor(); + + $version = $cap->getVersion(); + if (isset($version)) { + $infos []= $version; + } + $infos []= 'Supports STARTTLS: ' . ($cap->hasStartTls() ? 'True' : 'False'); + $infos []= 'Default naming context: ' . $cap->getDefaultNamingContext(); + $this->info['Discovery Results:'] = $infos; + } catch (Exception $e) { + $this->logInfo('Schema discovery not possible: ', $e->getMessage()); + } + + $this->healthy = true; + } + /** * Set up how to handle StartTLS connections * @@ -1013,56 +1082,6 @@ class LdapConnection implements Selectable } } - /** - * Discover the capabilities of the given LDAP server - * - * @param resource $ds The link identifier of the current LDAP connection - * - * @return Capability - * - * @throws LdapException In case the capability query has failed - */ - protected function discoverCapabilities($ds) - { - $fields = array( - 'defaultNamingContext', - 'namingContexts', - 'vendorName', - 'vendorVersion', - 'supportedSaslMechanisms', - 'dnsHostName', - 'schemaNamingContext', - 'supportedLDAPVersion', // => array(3, 2) - 'supportedCapabilities', - 'supportedControl', - 'supportedExtension', - '+' - ); - - $result = @ldap_read($ds, '', (string) $this->select()->from('*', $fields), $fields); - if (! $result) { - throw new LdapException( - '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( - 'Capabilities not available (%s:%d): %s. Discovery of root DSE probably not permitted.', - $this->hostname, - $this->port, - ldap_error($ds) - ); - } - - return new Capability($this->cleanupAttributes(ldap_get_attributes($ds, $entry), array_flip($fields))); - } - /** * Create an LDAP entry * @@ -1128,6 +1147,45 @@ class LdapConnection implements Selectable return $dir; } + protected function logInfo($message) + { + Logger::debug($message); + if (! isset($this->info)) { + $this->info = array(); + } + $this->info[] = $message; + } + + /** + * Get information about this objects state + * + * @return array An array of strings that describe the state in a human-readable form, each array element + * represents one fact about this object + */ + public function getInfo() + { + if (! isset($this->info)) { + $this->testConnectionHealth(); + } + return $this->info; + } + + /** + * If this object is working in its current configuration + * + * @return Bool True if the object is working, false if not + */ + public function isHealthy() + { + if (! isset($this->healthy)) { + try { + $this->testConnectionHealth(); + } catch (Exception $e) { + } + } + return $this->healthy; + } + /** * Reset the environment variables set by self::prepareTlsEnvironment() */