Matthias Jentsch 56e47fd084 Add an ini writer for configuration files
Add an ini writer that respects the file structure and the comments that may be
already present in the config file. Move Application/Config.php into
Config/Config.php.

refs #4352
2013-08-07 15:58:42 +02:00

471 lines
14 KiB
PHP

<?php
/**
* LDAP connection handler
*/
namespace Icinga\Protocol\Ldap;
use Icinga\Application\Platform;
use Icinga\Config\Config;
use Icinga\Application\Logger as Log;
/**
* Backend class managing all the LDAP stuff for you.
*
* Usage example:
*
* <code>
* $lconf = new Connection((object) array(
* 'hostname' => 'localhost',
* 'root_dn' => 'dc=monitoring,dc=...',
* 'bind_dn' => 'cn=Mangager,dc=monitoring,dc=...',
* 'bind_pw' => '***'
* ));
* </code>
*
* @copyright Copyright (c) 2013 Icinga-Web Team <info@icinga.org>
* @author Icinga-Web Team <info@icinga.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
*/
class Connection
{
const LDAP_NO_SUCH_OBJECT = 0x20;
protected $ds;
protected $hostname;
protected $port = 389;
protected $bind_dn;
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
// '1.3.6.1.4.1.4203.1.11.3' => '11.3', // Whoami
// '1.3.6.1.1.8' => '8', // Cancel Extended Request
);
protected $ms_capability = array(
// Prefix LDAP_CAP_
// Source: http://msdn.microsoft.com/en-us/library/cc223359.aspx
// Running Active Directory as AD DS:
'1.2.840.113556.1.4.800' => 'ACTIVE_DIRECTORY_OID',
// Capable of signing and sealing on an NTLM authenticated connection
// and of performing subsequent binds on a signed or sealed connection.
'1.2.840.113556.1.4.1791' => 'ACTIVE_DIRECTORY_LDAP_INTEG_OID',
// If AD DS: running at least W2K3, if AD LDS running at least W2K8
'1.2.840.113556.1.4.1670' => 'ACTIVE_DIRECTORY_V51_OID',
// If AD LDS: accepts DIGEST-MD5 binds for AD LDSsecurity principals
'1.2.840.113556.1.4.1880' => 'ACTIVE_DIRECTORY_ADAM_DIGEST',
// Running Active Directory as AD LDS
'1.2.840.113556.1.4.1851' => 'ACTIVE_DIRECTORY_ADAM_OID',
// If AD DS: it's a Read Only DC (RODC)
'1.2.840.113556.1.4.1920' => 'ACTIVE_DIRECTORY_PARTIAL_SECRETS_OID',
// Running at least W2K8
'1.2.840.113556.1.4.1935' => 'ACTIVE_DIRECTORY_V60_OID',
// Running at least W2K8r2
'1.2.840.113556.1.4.2080' => 'ACTIVE_DIRECTORY_V61_R2_OID',
// Running at least W2K12
'1.2.840.113556.1.4.2237' => 'ACTIVE_DIRECTORY_W8_OID',
);
protected $root;
protected $supports_v3 = false;
protected $supports_tls = false;
/**
* Constructor
*
* TODO: Allow to pass port and SSL options
*
* @param array LDAP connection credentials
*/
public function __construct($config)
{
$this->hostname = $config->hostname;
$this->bind_dn = $config->bind_dn;
$this->bind_pw = $config->bind_pw;
$this->root_dn = $config->root_dn;
}
public function getDN()
{
return $this->root_dn;
}
public function root()
{
if ($this->root === null) {
$this->root = Root::forConnection($this);
}
return $this->root;
}
public function select()
{
return new Query($this);
}
public function fetchOne($query, $fields = array())
{
$row = (array) $this->fetchRow($query, $fields);
return array_shift($row);
}
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 key($rows);
}
public function fetchRow($query, $fields = array())
{
// TODO: This is ugly, make it better!
$results = $this->fetchAll($query, $fields);
return array_shift($results);
}
public function count(Query $query)
{
$results = $this->runQuery($query, '+');
if (! $results) {
return 0;
}
return ldap_count_entries($this->ds, $results);
}
public function fetchAll($query, $fields = array())
{
$offset = null;
$limit = null;
if ($query->hasLimit()) {
$offset = $query->getOffset();
$limit = $query->getLimit();
}
$entries = array();
$results = $this->runQuery($query, $fields);
if (! $results) {
return array();
}
$entry = ldap_first_entry($this->ds, $results);
$count = 0;
while ($entry) {
if (($offset === null || $offset <= $count)
&& ($limit === null || ($offset + $limit) >= $count)
) {
$attrs = ldap_get_attributes($this->ds, $entry);
$entries[ldap_get_dn($this->ds, $entry)]
= $this->cleanupAttributes($attrs);
}
$count++;
$entry = ldap_next_entry($this->ds, $entry);
}
ldap_free_result($results);
return $entries;
}
public function cleanupAttributes(& $attrs)
{
$clean = (object) array();
for ($i = 0; $i < $attrs['count']; $i++) {
$attr_name = $attrs[$i];
if ($attrs[$attr_name]['count'] === 1) {
$clean->$attr_name = $attrs[$attr_name][0];
} else {
for ($j = 0; $j < $attrs[$attr_name]['count']; $j++) {
$clean->{$attr_name}[] = $attrs[$attr_name][$j];
}
}
}
return $clean;
}
protected function runQuery($query, $fields)
{
$this->connect();
if ($query instanceof Query) {
$fields = $query->listFields();
}
// WARNING:
// We do not support pagination right now, and there is no chance to
// do so for PHP < 5.4. Warnings about "Sizelimit exceeded" will
// therefore not be hidden right now.
$results = @ldap_search(
$this->ds,
$this->root_dn,
(string) $query,
$fields,
0, // Attributes and values
0 // No limit - at least where possible
);
if ($results === false) {
if (ldap_errno($this->ds) === self::LDAP_NO_SUCH_OBJECT) {
return false;
}
throw new \Exception(
sprintf(
'LDAP query "%s" (root %s) failed: %s',
$query,
$this->root_dn,
ldap_error($this->ds)
)
);
die('Query failed');
}
$list = array();
if ($query instanceof Query) {
foreach ($query->getSortColumns() as $col) {
ldap_sort($this->ds, $results, $col[0]);
}
}
return $results;
}
public function testCredentials($username, $password)
{
$ds = $this->prepareNewConnection();
$r = @ldap_bind($ds, $username, $password);
if ($r) {
log::debug(
'Successfully tested LDAP credentials (%s / %s)',
$username,
'***'
);
return true;
} else {
log::debug(
'Testing LDAP credentials (%s / %s) failed: %s',
$username,
'***',
ldap_error($ds)
);
return false;
}
}
protected function getConfigDir($sub = null)
{
$dir = Config::getInstance()->getConfigDir() . '/ldap';
if ($sub !== null) {
$dir .= '/' . $sub;
}
return $dir;
}
protected function discoverServerlistForDomain($domain)
{
$ldaps_records = dns_get_record('_ldaps._tcp.' . $domain, DNS_SRV);
$ldap_records = dns_get_record('_ldap._tcp.' . $domain, DNS_SRV);
}
protected function prepareNewConnection()
{
$use_tls = false;
$force_tls = true;
$force_tls = false;
if ($use_tls) {
$this->prepareTlsEnvironment();
}
$ds = ldap_connect($this->hostname, $this->port);
$cap = $this->discoverCapabilities($ds);
if ($use_tls) {
if ($cap->starttls) {
if (@ldap_start_tls($ds)) {
Log::debug('LDAP STARTTLS succeeded');
} else {
Log::debug('LDAP STARTTLS failed: %s', ldap_error($ds));
throw new \Exception(
sprintf(
'LDAP STARTTLS failed: %s',
ldap_error($ds)
)
);
}
} elseif ($force_tls) {
throw new \Exception(
sprintf(
'TLS is required but not announced by %s',
$this->host_name
)
);
} else {
// TODO: Log noticy -> TLS enabled but not announced
}
}
// ldap_rename requires LDAPv3:
if ($cap->ldapv3) {
if (! ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)) {
throw new Exception('LDAPv3 is required');
}
} else {
// TODO: remove this -> FORCING v3 for now
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
Log::warn('No LDAPv3 support detected');
}
// Not setting this results in "Operations error" on AD when using the
// whole domain as search base:
ldap_set_option($ds, LDAP_OPT_REFERRALS, 0);
// ldap_set_option($ds, LDAP_OPT_DEREF, LDAP_DEREF_NEVER);
return $ds;
}
protected function prepareTlsEnvironment()
{
$strict_tls = true;
// TODO: allow variable known CA location (system VS Icinga)
if (Platform::isWindows()) {
// putenv('LDAP...')
} else {
if ($strict_tls) {
$ldap_conf = $this->getConfigDir('ldap_ca.conf');
} else {
$ldap_conf = $this->getConfigDir('ldap_nocert.conf');
}
putenv('LDAPRC=' . $ldap_conf);
if (getenv('LDAPRC') !== $ldap_conf) {
throw new Exception('putenv failed');
}
}
}
protected function discoverCapabilities($ds)
{
$query = $this->select()->from(
'*',
array(
'defaultNamingContext',
'namingContexts',
'vendorName',
'vendorVersion',
'supportedSaslMechanisms',
'dnsHostName',
'schemaNamingContext',
'supportedLDAPVersion', // => array(3, 2)
'supportedCapabilities',
'supportedExtension',
'+'
)
);
$fields = $query->listFields();
$result = @ldap_read(
$ds,
'',
(string) $query,
$query->listFields()
);
if (! $result) {
throw new Exception(
sprintf(
'Capability query failed: %s',
ldap_error($ds)
)
);
}
$entry = ldap_first_entry($ds, $result);
$cap = (object) array(
'ldapv3' => false,
'starttls' => false,
);
if ($entry === false) {
// TODO: Is it OK to have no capabilities?
return $cap;
}
$ldapAttributes = ldap_get_attributes($ds, $entry);
$result = $this->cleanupAttributes(
$ldapAttributes
);
/*
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()
{
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)
)
);
}
}
public function __destruct()
{
putenv('LDAPRC');
}
}