diff options
Diffstat (limited to 'library/Icinga/Protocol/Ldap/LdapCapabilities.php')
-rw-r--r-- | library/Icinga/Protocol/Ldap/LdapCapabilities.php | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/library/Icinga/Protocol/Ldap/LdapCapabilities.php b/library/Icinga/Protocol/Ldap/LdapCapabilities.php new file mode 100644 index 0000000..721655a --- /dev/null +++ b/library/Icinga/Protocol/Ldap/LdapCapabilities.php @@ -0,0 +1,440 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Protocol\Ldap; + +use Icinga\Application\Logger; +use stdClass; + +/** + * The properties and capabilities of an LDAP server + * + * Provides information about the available encryption mechanisms (StartTLS), the supported + * LDAP protocol (v2/v3), vendor-specific extensions or protocols controls and extensions. + */ +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'; + + const LDAP_SERVER_SHOW_DELETED_OID = '1.2.840.113556.1.4.417'; + + const LDAP_SERVER_SORT_OID = '1.2.840.113556.1.4.473'; + + const LDAP_SERVER_CROSSDOM_MOVE_TARGET_OID = '1.2.840.113556.1.4.521'; + + const LDAP_SERVER_NOTIFICATION_OID = '1.2.840.113556.1.4.528'; + + const LDAP_SERVER_EXTENDED_DN_OID = '1.2.840.113556.1.4.529'; + + const LDAP_SERVER_LAZY_COMMIT_OID = '1.2.840.113556.1.4.619'; + + const LDAP_SERVER_SD_FLAGS_OID = '1.2.840.113556.1.4.801'; + + const LDAP_SERVER_TREE_DELETE_OID = '1.2.840.113556.1.4.805'; + + const LDAP_SERVER_DIRSYNC_OID = '1.2.840.113556.1.4.841'; + + const LDAP_SERVER_VERIFY_NAME_OID = '1.2.840.113556.1.4.1338'; + + const LDAP_SERVER_DOMAIN_SCOPE_OID = '1.2.840.113556.1.4.1339'; + + const LDAP_SERVER_SEARCH_OPTIONS_OID = '1.2.840.113556.1.4.1340'; + + const LDAP_SERVER_PERMISSIVE_MODIFY_OID = '1.2.840.113556.1.4.1413'; + + const LDAP_SERVER_ASQ_OID = '1.2.840.113556.1.4.1504'; + + const LDAP_SERVER_FAST_BIND_OID = '1.2.840.113556.1.4.1781'; + + const LDAP_CONTROL_VLVREQUEST = '2.16.840.1.113730.3.4.9'; + + + // MS Capabilities, Source: http://msdn.microsoft.com/en-us/library/cc223359.aspx + + // Running Active Directory as AD DS + const LDAP_CAP_ACTIVE_DIRECTORY_OID = '1.2.840.113556.1.4.800'; + + // Capable of signing and sealing on an NTLM authenticated connection + // and of performing subsequent binds on a signed or sealed connection + const LDAP_CAP_ACTIVE_DIRECTORY_LDAP_INTEG_OID = '1.2.840.113556.1.4.1791'; + + // If AD DS: running at least W2K3, if AD LDS running at least W2K8 + const LDAP_CAP_ACTIVE_DIRECTORY_V51_OID = '1.2.840.113556.1.4.1670'; + + // If AD LDS: accepts DIGEST-MD5 binds for AD LDSsecurity principals + const LDAP_CAP_ACTIVE_DIRECTORY_ADAM_DIGEST = '1.2.840.113556.1.4.1880'; + + // Running Active Directory as AD LDS + const LDAP_CAP_ACTIVE_DIRECTORY_ADAM_OID = '1.2.840.113556.1.4.1851'; + + // If AD DS: it's a Read Only DC (RODC) + const LDAP_CAP_ACTIVE_DIRECTORY_PARTIAL_SECRETS_OID = '1.2.840.113556.1.4.1920'; + + // Running at least W2K8 + const LDAP_CAP_ACTIVE_DIRECTORY_V60_OID = '1.2.840.113556.1.4.1935'; + + // Running at least W2K8r2 + const LDAP_CAP_ACTIVE_DIRECTORY_V61_R2_OID = '1.2.840.113556.1.4.2080'; + + // Running at least W2K12 + const LDAP_CAP_ACTIVE_DIRECTORY_W8_OID = '1.2.840.113556.1.4.2237'; + + /** + * Attributes of the LDAP Server returned by the discovery query + * + * @var stdClass + */ + private $attributes; + + /** + * Map of supported available OIDS + * + * @var array + */ + private $oids; + + /** + * Construct a new capability + * + * @param $attributes stdClass The attributes returned, may be null for guessing default capabilities + */ + public function __construct($attributes = null) + { + $this->setAttributes($attributes); + } + + /** + * Set the attributes and (re)build the OIDs + * + * @param $attributes stdClass The attributes returned, may be null for guessing default capabilities + */ + protected function setAttributes($attributes) + { + $this->attributes = $attributes; + $this->oids = array(); + + $keys = array('supportedControl', 'supportedExtension', 'supportedFeatures', 'supportedCapabilities'); + foreach ($keys as $key) { + if (isset($attributes->$key)) { + if (is_array($attributes->$key)) { + foreach ($attributes->$key as $oid) { + $this->oids[$oid] = true; + } + } else { + $this->oids[$attributes->$key] = true; + } + } + } + } + + /** + * Return if the capability object contains support for StartTLS + * + * @return bool Whether StartTLS is supported + */ + public function hasStartTls() + { + return isset($this->oids[self::LDAP_SERVER_START_TLS_OID]); + } + + /** + * Return if the capability object contains support for paged results + * + * @return bool Whether StartTLS is supported + */ + public function hasPagedResult() + { + return isset($this->oids[self::LDAP_PAGED_RESULT_OID_STRING]); + } + + /** + * Whether the ldap server is an ActiveDirectory server + * + * @return boolean + */ + 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 + * + * @return bool + */ + public function hasLdapV3() + { + if (! isset($this->attributes) || ! isset($this->attributes->supportedLDAPVersion)) { + // Default to true, if unknown + return true; + } + + return (is_string($this->attributes->supportedLDAPVersion) + && (int) $this->attributes->supportedLDAPVersion === 3) + || (is_array($this->attributes->supportedLDAPVersion) + && in_array(3, $this->attributes->supportedLDAPVersion)); + } + + /** + * Whether the capability with the given OID is supported + * + * @param $oid string The OID of the capability + * + * @return bool + */ + public function hasOid($oid) + { + return isset($this->oids[$oid]); + } + + /** + * Get the default naming context + * + * @return string|null the default naming context, or null when no contexts are available + */ + public function getDefaultNamingContext() + { + // defaultNamingContext entry has higher priority + if (isset($this->attributes->defaultNamingContext)) { + return $this->attributes->defaultNamingContext; + } + + // if its missing use namingContext + $namingContexts = $this->namingContexts(); + return empty($namingContexts) ? null : $namingContexts[0]; + } + + /** + * Get the configuration naming context + * + * @return string|null + */ + public function getConfigurationNamingContext() + { + if (isset($this->attributes->configurationNamingContext)) { + return $this->attributes->configurationNamingContext; + } + } + + /** + * Get the NetBIOS name + * + * @return string|null + */ + public function getNetBiosName() + { + if (isset($this->attributes->nETBIOSName)) { + return $this->attributes->nETBIOSName; + } + } + + /** + * Fetch the namingContexts + * + * @return array the available naming contexts + */ + public function namingContexts() + { + if (!isset($this->attributes->namingContexts)) { + return array(); + } + if (!is_array($this->attributes->namingContexts)) { + return array($this->attributes->namingContexts); + } + 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 + * + * @return LdapCapabilities + * + * @throws LdapException In case the capability query has failed + */ + public static function discoverCapabilities(LdapConnection $connection) + { + $ds = $connection->getConnection(); + + $fields = array( + 'configurationNamingContext', + '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; Default port: %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; Default port: %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), $fields)); + $cap->discoverAdConfigOptions($connection); + + if (isset($cap->attributes) && Logger::getInstance()->getLevel() === Logger::DEBUG) { + Logger::debug('Capability query discovered the following attributes:'); + foreach ($cap->attributes as $name => $value) { + if ($value !== null) { + Logger::debug(' %s = %s', $name, $value); + } + } + Logger::debug('Capability query attribute listing ended.'); + } + + return $cap; + } + + /** + * Discover the AD-specific configuration options of the given LDAP server + * + * @param LdapConnection $connection The ldap connection to use + * + * @throws LdapException In case the configuration options query has failed + */ + protected function discoverAdConfigOptions(LdapConnection $connection) + { + if ($this->isActiveDirectory()) { + $configurationNamingContext = $this->getConfigurationNamingContext(); + $defaultNamingContext = $this->getDefaultNamingContext(); + if (!($configurationNamingContext === null || $defaultNamingContext === null)) { + $ds = $connection->bind()->getConnection(); + $adFields = array('nETBIOSName'); + $partitions = 'CN=Partitions,' . $configurationNamingContext; + + $result = @ldap_list( + $ds, + $partitions, + (string) $connection->select()->from('*', $adFields)->where('nCName', $defaultNamingContext), + $adFields + ); + if ($result) { + $entry = ldap_first_entry($ds, $result); + if ($entry === false) { + throw new LdapException( + 'Configuration options not available (%s:%d). Discovery of "%s" probably not permitted.', + $connection->getHostname(), + $connection->getPort(), + $partitions + ); + } + + $this->setAttributes((object) array_merge( + (array) $this->attributes, + (array) $connection->cleanupAttributes(ldap_get_attributes($ds, $entry), $adFields) + )); + } else { + if (ldap_errno($ds) !== 1) { + // One stands for "operations error" which occurs if not bound non-anonymously. + + throw new LdapException( + 'Configuration options 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) + ); + } + } + } + } + } + + /** + * 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; + } +} |