Merge branch 'feature/ldap-connection-test-function-9605'

fixes #9605
This commit is contained in:
Matthias Jentsch 2015-07-15 10:50:02 +02:00
commit 6db80f1e74
5 changed files with 290 additions and 80 deletions

View File

@ -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';

View File

@ -0,0 +1,26 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data;
/**
* An object for which the user can retrieve status information
*/
interface Inspectable
{
/**
* 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 this object is working in its current configuration
*
* @return Bool True if the object is working, false if not
*/
public function isHealthy();
}

View File

@ -66,7 +66,7 @@ class Discovery {
*/
public function isAd()
{
return $this->connection->getCapabilities()->hasAdOid();
return $this->connection->getCapabilities()->isActiveDirectory();
}
/**

View File

@ -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;
}
}

View File

@ -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()
*/