From c4a6c2c3d0509d2d1a27a095b87073b1975e2527 Mon Sep 17 00:00:00 2001 From: Olav Morken <olav.morken@uninett.no> Date: Mon, 21 Nov 2011 13:01:44 +0000 Subject: [PATCH] ldap: Common base authproc filter + new filter. This patch adds a common "base filter" for creating authproc filters that access a LDAP directory, and it adds a filter (ldap:AttributeAddUsersGroups) for adding group information. Thanks to Ryan Panning for implementing this! git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@2986 44740490-163a-0410-bde0-09ae8108e29a --- lib/SimpleSAML/Auth/LDAP.php | 111 +++++- modules/ldap/docs/ldap.txt | 324 +++++++++++++++++- .../lib/Auth/Process/AttributeAddFromLDAP.php | 234 ++++--------- .../Auth/Process/AttributeAddUsersGroups.php | 295 ++++++++++++++++ modules/ldap/lib/Auth/Process/BaseFilter.php | 277 +++++++++++++++ 5 files changed, 1076 insertions(+), 165 deletions(-) create mode 100644 modules/ldap/lib/Auth/Process/AttributeAddUsersGroups.php create mode 100644 modules/ldap/lib/Auth/Process/BaseFilter.php diff --git a/lib/SimpleSAML/Auth/LDAP.php b/lib/SimpleSAML/Auth/LDAP.php index 828f97fa5..e1cc2282a 100644 --- a/lib/SimpleSAML/Auth/LDAP.php +++ b/lib/SimpleSAML/Auth/LDAP.php @@ -48,9 +48,10 @@ class SimpleSAML_Auth_LDAP { * @param bool $enable_tls * @param bool $debug * @param int $timeout + * @param int $port */ // TODO: Flesh out documentation. - public function __construct($hostname, $enable_tls = TRUE, $debug = FALSE, $timeout = 0) { + public function __construct($hostname, $enable_tls = TRUE, $debug = FALSE, $timeout = 0, $port = 389) { // Debug. SimpleSAML_Logger::debug('Library - LDAP __construct(): Setup LDAP with ' . @@ -72,7 +73,7 @@ class SimpleSAML_Auth_LDAP { * Prepare a connection for to this LDAP server. Note that this function * doesn't actually connect to the server. */ - $this->ldap = @ldap_connect($hostname); + $this->ldap = @ldap_connect($hostname, $port); if ($this->ldap == FALSE) throw new $this->makeException('Library - LDAP __construct(): Unable to connect to \'' . $hostname . '\'', ERR_INTERNAL); @@ -281,6 +282,85 @@ class SimpleSAML_Auth_LDAP { } + /** + * This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory() + * method, but could be used for other LDAP search needs. It will search LDAP and return all the entries. + * + * @throws Exception + * @param string|array $bases + * @param string|array $filters Array of 'attribute' => 'values' to be combined into the filter, or a raw filter string + * @param string|array $attributes Array of attributes requested from LDAP + * @param bool $and If multiple filters defined, then either bind them with & or | + * @param bool $escape Weather to escape the filter values or not + * @return array + */ + public function searchformultiple($bases, $filters, $attributes = array(), $and = TRUE, $escape = TRUE) { + + // Escape the filter values, if requested + if ($escape) { + $filters = $this->escape_filter_value($filters, FALSE); + } + + // Build search filter + $filter = ''; + if (is_array($filters)) { + foreach ($filters as $attribute => $value) { + $filter .= "($attribute=$value)"; + } + if (count($filters) > 1) { + $filter = ($and ? '(&' : '(|') . $filter . ')'; + } + } elseif (is_string($filters)) { + $filter = $filters; + } + + // Verify filter was created + if ($filter == '' || $filter == '(=)') { + throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL); + } + + // Verify at least one base was passed + $bases = (array) $bases; + if (empty($bases)) { + throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL); + } + + // Search each base until result is found + $result = FALSE; + foreach ($bases as $base) { + $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout); + if ($result !== FALSE) break; + } + + // Verify that a result was found in one of the bases + if ($result === FALSE) { + throw $this->makeException( + 'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) [' . + implode('; ', $bases) . '] with filter [' . $filter . ']. LDAP error [' . + ldap_error($this->ldap) . ']' + ); + } elseif (@ldap_count_entries($this->ldap, $result) < 1) { + throw $this->makeException( + 'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) [' . + implode('; ', $bases) . '] with filter [' . $filter . ']', + ERR_NO_USER + ); + } + + // Get all results + $results = ldap_get_entries($this->ldap, $result); + if ($results === FALSE) { + throw $this->makeException( + 'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results' + ); + } + + // Remove the count and return + unset($results['count']); + return $results; + } + + /** * Bind to LDAP with a specific DN and password. Simple wrapper around * ldap_bind() with some additional logging. @@ -349,6 +429,33 @@ class SimpleSAML_Auth_LDAP { } + /** + * Applies an LDAP option to the current connection. + * + * @throws Exception + * @param $option + * @param $value + * @return void + */ + public function setOption($option, $value) { + + // Attempt to set the LDAP option + if (!@ldap_set_option($this->ldap, $option, $value)) { + throw $this->makeException( + 'ldap:LdapConnection->setOption : Failed to set LDAP option [' . + $option . '] with the value [' . $value . '] error: ' . ldap_error($this->ldap), + ERR_INTERNAL + ); + } + + // Log debug message + SimpleSAML_Logger::debug( + 'ldap:LdapConnection->setOption : Set the LDAP option [' . + $option . '] with the value [' . $value . ']' + ); + } + + /** * Search a given DN for attributes, and return the resulting associative * array. diff --git a/modules/ldap/docs/ldap.txt b/modules/ldap/docs/ldap.txt index 2cdf47754..946c3737d 100644 --- a/modules/ldap/docs/ldap.txt +++ b/modules/ldap/docs/ldap.txt @@ -2,7 +2,8 @@ LDAP module =========== The LDAP module provides a method for authenticating users against a -LDAP server. There are two seperate authentication modules: +LDAP server. There are two separate authentication modules and two +authentication process filters: `ldap:LDAP` @@ -11,6 +12,11 @@ LDAP server. There are two seperate authentication modules: `ldap:LDAPMulti` : Allow the user to chose one LDAP server to authenticate against. +`ldap:AttributeAddFromLDAP` +: Adds an attribute value from LDAP to the request + +`ldap:AttributeAddUsersGroups` +: Add a attribute in the request with all the users group memberships `ldap:LDAP` ----------- @@ -220,3 +226,319 @@ All options from the `ldap:LDAP` configuration can be used in each group, and you should refer to the documentation for that module for more information about available options. + +`ldap:AttributeAddFromLDAP` +------------------------------ + +Filter to add attributes to the identity by executing a query against +an LDAP directory. In addition to all the configuration options available +in the ldap:AttributeAddUsersGroups filter (below), these are the filter +specific configuration options: + + + 50 = array( + 'class' => 'ldap:AttributeAddFromLDAP', + + + /** + * The attribute name used when adding the LDAP values + * to the request attributes. + * + * Default: NULL + * Require: Yes + */ + 'attribute.new' => 'my_ldap_attrib', + + + /** + * When searching LDAP, this is the attribute to retrieve + * and add to the request attributes. + * + * Default: NULL + * Require: Yes + */ + 'search.attribute' => 'displayName', + + + /** + * The attribute name used when adding the LDAP values + * to the request attributes. + * + * Note: Variable substitution will be performed on this option. + * Any attribute in the identity can be substituted by surrounding + * it with percent symbols (%). For instance %cn% would be replaced + * with the CN of the user. + * + * Default: NULL + * Require: Yes + */ + 'search.filter' => '(uniquemember=cn=%cn%,cn=users,cn=example,dc=org)', + ); + + +### Backwards Compatability ### + +The filter option names have recently changed, however the old config names will be +converted to the new names automatically. That way any existing/older config's +should still work. Below are the old config names and their new names: + +array( + 'ldap_host' => 'ldap.hostname', + 'ldap_port' => 'ldap.port', + 'ldap_bind_user' => 'ldap.username', + 'ldap_bind_pwd' => 'ldap.password', + 'userid_attribute' => 'attribute.username', + 'ldap_search_base_dn' => 'ldap.basedn', + 'ldap_search_filter' => 'search.filter', + 'ldap_search_attribute' => 'search.attribute', + 'new_attribute_name' => 'attribute.new' +) + + +### Example ### + +This is the most basic configuration possible. It will look at the +authsource for all LDAP connection information and queries LDAP for +the specific attribute. + + 50 => array( + 'class' => 'ldapAttributeAddUsersGroups', + 'authsource' => 'example-ldap', + 'attribute.new' => 'my_ldap_attribute', + 'search.attribute' => 'displayName' + 'search.filter' => '(uniquemember=cn=%cn%,cn=users,cn=example,dc=org)' + ) + +If no authsource is available then you can specify the connection info +using the filter configuration. Note: All of the options below are not +required, see the config options for ldap:AttributeAddUsersGroups below. + + 50 => array( + 'class' => 'ldapAttributeAddUsersGroups', + 'ldap.hostname' => 'ldap.example.org', + 'ldap.username' => 'CN=LDAP User,CN=Users,DC=example,DC=org', + 'ldap.password' => 'Abc123', + 'ldap.basedn' => 'DC=example,DC=org', + 'attribute.new' => 'my_ldap_attribute', + 'search.attribute' => 'displayName' + 'search.filter' => '(uniquemember=cn=%cn%,cn=users,cn=example,dc=org)' + ) + + + + +`ldap:AttributeAddUsersGroups` +------------------------------ + +This filter will add the logged in users LDAP group memberships to +a specified request attribute. Although most LDAP products have a +memberOf attribute which only lists the direct membership relations, +this filter checks those relation for "sub" groups, recursively +checking the hierarchy for all groups the user would technically be +a member of. This can be helpful for other filters to know. Below is +a listing of all configuration options and their details. + + + 50 => array( + 'class' => 'ldap:AttributeAddUsersGroups', + + + /** + * LDAP connection settings can be retrieved from an ldap:LDAP + * authsource. Specify the authsource name here to pull that + * data from the authsources.php file in the config folder. + * + * Note: ldap:LDAPMulti is not supported as the SimpleSAMLphp + * framework does not pass any information about which + * LDAP source the user selected. + * + * Default: NULL + * Require: No + */ + 'authsource' => NULL, + 'authsource' => 'example-ldap', + + + /** + * This is the attribute name which the users groups will be + * added to. If the attribute exists in the request then the + * filter will attempt to add them. + * + * Default: 'groups' + * Required: No + */ + 'attribute.groups' => 'groups', + + + /** + * The base DN used to search LDAP. May not be needed if searching + * LDAP using the standard method, meaning that no Product is specified. + * Can be listed as a single string for one base, else an array of + * strings for multiple bases. + * + * Default: '' + * Required: No + * AuthSource: search.base + */ + 'ldap.basedn' => '', + 'ldap.basedn' => 'DC=example,DC=org', + 'ldap.basedn' => array( + 'OU=Staff,DC=example,DC=org', + 'OU=Students,DC=example,DC=org' + ), + + + /** + * Set to TRUE to enable LDAP debug level. Passed to + * the LDAP connection class. + * + * Default: FALSE + * Required: No + * AuthSource: debug + */ + 'ldap.debug' => FALSE, + 'ldap.debug' => TRUE, + + + /** + * Set to TRUE to force the LDAP connection to use TLS. + * + * Note: If ldaps:// is specified in the hostname then it + * will automatically use TLS. + * + * Default: FALSE + * Required: No + * AuthSource: enable_tls + */ + 'ldap.enable_tls' => FALSE, + 'ldap.enable_tls' => TRUE, + + + /** + * This is the hostname string of LDAP server(s) to try + * and connect to. It should be the same format as the + * LDAP authsource hostname as it is passed to that class. + * + * Note: Multiple servers are separated by a space. + * + * Default: NULL + * Required: Yes, unless authsource is used + * AuthSource: hostname + */ + 'ldap.hostname' => 'ldap.example.org', + 'ldap.hostname' => 'ad1.example.org ad2.example.org', + + + /** + * This is the password used to bind to LDAP. + * + * Default: NULL + * Required: No, only if required for binding. + * AuthSource: search.password OR priv.password + */ + 'ldap.password' => 'Abc123', + + + /** + * By specifying the directory service product name, the number + * of LDAP queries can be dramatically reduced. The reason is + * that most products have a special query to recursively search + * group membership. + * + * Note: Only ActiveDirectory is currently supported. + * + * Default: '' + * Required: No + */ + 'ldap.product' => '', + 'ldap.product' => 'ActiveDirectory', + + + /** + * The LDAP timeout value passed to the LDAP connection class. + * + * Default: 0 + * Required: No + * AuthSource: timeout + */ + 'ldap.timeout' => 0, + 'ldap.timeout' => 30, + + + /** + * This is the username used to bind to LDAP with. + * More than likely will need to be in the DN of + * user binding to LDAP. + * + * Default: NULL + * Required: No, only if required for binding. + * AuthSource: search.username OR priv.username + */ + 'ldap.username' => 'CN=LDAP User,CN=Users,DC=example,DC=org', + + + /** + * The following attribute.* and type.* configuration options + * define the LDAP schema and should only be defined/modified + * if the schema has been modified or the LDAP product used + * uses other attribute names. By default, the schema is setup + * for ActiveDirectory. + * + * Defaults: Listed Below + * Required: No + */ + 'attribute.dn' => 'distinguishedName', + 'attribute.groups' => 'groups', // Also noted above + 'attribute.member' => 'member', + 'attribute.memberof' => 'memberOf', + 'attribute.groupname' => 'name', + 'attribute.type' => 'objectClass', + 'attribute.username' => 'sAMAccountName', + + + /** + * As mentioned above, these can be changed if the LDAP schema + * has been modified. These list the Object/Entry Type for a given + * DN, in relation to the 'attribute.type' config option above. + * These are used to determine the type of entry. + * + * Defaults: Listed Below + * Required: No + */ + 'type.group' => 'group', + 'type.user' => 'user', + ) + + +### Example ### + +This is the most basic configuration possible. It will look at the +authsource for all LDAP connection information and manually search +the hierarchy for the users group memberships. + + 50 => array( + 'class' => 'ldapAttributeAddUsersGroups', + 'authsource' => 'example-ldap' + ) + +By making one small change we can optimize the filter to use better +group search methods and eliminate un-needed LDAP queries. + + 50 => array( + 'class' => 'ldapAttributeAddUsersGroups', + 'authsource' => 'example-ldap', + 'ldap.product' => 'ActiveDirectory' + ) + +If no authsource is available then you can specify the connection info +using the filter configuration. Note: All of the options below are not +required, see the config info above for details. + + 50 => array( + 'class' => 'ldapAttributeAddUsersGroups', + 'ldap.hostname' => 'ldap.example.org', + 'ldap.username' => 'CN=LDAP User,CN=Users,DC=example,DC=org', + 'ldap.password' => 'Abc123', + 'ldap.basedn' => 'DC=example,DC=org' + ) + diff --git a/modules/ldap/lib/Auth/Process/AttributeAddFromLDAP.php b/modules/ldap/lib/Auth/Process/AttributeAddFromLDAP.php index fc08fb3c7..c731a2f06 100644 --- a/modules/ldap/lib/Auth/Process/AttributeAddFromLDAP.php +++ b/modules/ldap/lib/Auth/Process/AttributeAddFromLDAP.php @@ -1,101 +1,53 @@ <?php -// +---------------------------------------------------+ -// | PHP Version: 5.2.x | -// +---------------------------------------------------+ -// | simpleSAMLphp Auth Proc for adding additional | -// | identity attributes from a secondary LDAP query | -// +---------------------------------------------------+ -// | | -// | This Auth Proc needs the following configuration | -// | directives set in order to function properly. | -// | | -// | 'ldap_host' the hostname of the LDAP server | -// | | -// | 'ldap_port' the port for the LDAP process | -// | | -// | 'ldap_bind_user' the user to bind as (optional) | -// | | -// | 'ldap_bind_pwd' the password for the bind user | -// | required if ldap_bind_user is | -// | specified | -// | | -// | 'userid_attribute' the attribute you will use to | -// | filter results | -// | | -// | 'ldap_search_base_dn' the search base | -// | | -// | 'ldap_search_filter' the search filter. | -// | NOTE: Variable substitution will be performed on | -// | ldap_search_filter. Any attribute in the | -// | identity can be substituted by surrounding | -// | it with percent symbols (%). For instance | -// | %cn% would be replaced with the cn of the | -// | user. | -// | | -// | 'ldap_search_attribute' the name of the attribute | -// | in the search results that you want to add | -// | to the identity attributes | -// | | -// | 'new_attribute_name' the name you want the newly | -// | added attribute to be called when it's added | -// | | -// | EXAMPLE | -// | | -// | 'authproc' => array( -// | 50 => array( -// | 'class' => 'ldap:AttributeAddFromLDAP', -// | 'ldap_host' => 'ldap.example.org', -// | 'ldap_port' => '389', -// | 'ldap_bind_user' => 'ldap_bind_user', -// | 'ldap_bind_pwd' => 'ldap_bind_pwd', -// | 'userid_attribute' => 'cn', -// | 'ldap_search_base_dn' => 'cn=security_tags,dc=example,dc=org', -// | 'ldap_search_filter' => '(uniquemember=cn=%cn%,cn=users,cn=example,dc=org)', -// | 'ldap_search_attribute' => 'displayname', -// | 'new_attribute_name' => 'security_tags', -// | ), -// | ), -// | | -// | This will cause the Auth Proc to query the LDAP | -// | looking for all the security tags that the | -// | current user is assigned. It will take the value | -// | contained in displayname and put it into a mutli- | -// | value attribute called security_tags | -// | | -// +---------------------------------------------------+ -// | Author: Steve Moitozo II <steve_moitozo@jaars.org>| -// | Created: 20100513 | -// +---------------------------------------------------+ -// | 20100920 Steve Moitozo II | -// | - incorporated feedback from Olav Morken to | -// | prep code for inclusion in SimpleSAMLphp distro| -// | - moved call to ldap_set_options() inside test | -// | for $ds | -// | - added the output of ldap_error() to the | -// | exceptions | -// | - reduced some of the nested ifs | -// | - added support for multiple values | -// | - added support for anonymous binds | -// | - added escaping of search filter and attribute| -// +---------------------------------------------------+ - /** * Filter to add attributes to the identity by executing a query against an LDAP directory * + * Original Author: Steve Moitozo II <steve_moitozo@jaars.org> + * Created: 20100513 + * Updated: 20100920 Steve Moitozo II + * - incorporated feedback from Olav Morken to prep code for inclusion in SimpleSAMLphp distro + * - moved call to ldap_set_options() inside test for $ds + * - added the output of ldap_error() to the exceptions + * - reduced some of the nested ifs + * - added support for multiple values + * - added support for anonymous binds + * - added escaping of search filter and attribute + * Updated: 20111118 Ryan Panning + * - Updated the class to use BaseFilter which reuses LDAP connection features + * - Added conversion of original filter option names for backwards-compatibility + * - Updated the constructor to use the new config method + * - Updated the process method to use the new config variable names * - * @author Steve Moitozo, JAARS, Inc. + * @author Steve Moitozo, JAARS, Inc., Ryan Panning * @package simpleSAMLphp * @version $Id$ */ -class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends SimpleSAML_Auth_ProcessingFilter { +class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends sspmod_ldap_Auth_Process_BaseFilter { + /** - * The configuration. + * Name of the attribute to add LDAP values to * - * Associative array of strings. + * @var string */ - private $config = array(); + protected $new_attribute; + + + /** + * LDAP attribute to add to the request attributes + * + * @var string + */ + protected $search_attribute; + + + /** + * LDAP search filter to use in the LDAP query + * + * @var string + */ + protected $search_filter; /** @@ -105,51 +57,38 @@ class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends SimpleSAML_Auth_Proc * @param mixed $reserved For future use. */ public function __construct($config, $reserved) { - parent::__construct($config, $reserved); - assert('is_array($config)'); - - $reqConfigVars = array( - 'ldap_host', - 'ldap_port', - 'ldap_bind_user', - 'ldap_bind_pwd', - 'userid_attribute', - 'ldap_search_base_dn', - 'ldap_search_filter', - 'ldap_search_attribute', - 'new_attribute_name' + // For backwards compatibility, check for old config names + if (isset($config['ldap_host'])) $config['ldap.hostname'] = $config['ldap_host']; + if (isset($config['ldap_port'])) $config['ldap.port'] = $config['ldap_port']; + if (isset($config['ldap_bind_user'])) $config['ldap.username'] = $config['ldap_bind_user']; + if (isset($config['ldap_bind_pwd'])) $config['ldap.password'] = $config['ldap_bind_pwd']; + if (isset($config['userid_attribute'])) $config['attribute.username'] = $config['userid_attribute']; + if (isset($config['ldap_search_base_dn'])) $config['ldap.basedn'] = $config['ldap_search_base_dn']; + if (isset($config['ldap_search_filter'])) $config['search.filter'] = $config['ldap_search_filter']; + if (isset($config['ldap_search_attribute'])) $config['search.attribute'] = $config['ldap_search_attribute']; + if (isset($config['new_attribute_name'])) $config['attribute.new'] = $config['new_attribute_name']; + + // Remove the old config names + unset( + $config['ldap_host'], + $config['ldap_port'], + $config['ldap_bind_user'], + $config['ldap_bind_pwd'], + $config['userid_attribute'], + $config['ldap_search_base_dn'], + $config['ldap_search_filter'], + $config['ldap_search_attribute'], + $config['new_attribute_name'] ); - foreach($config as $name => $values){ - if(!is_string($name)){ - throw new Exception('Invalid attribute name: ' . var_export($name, TRUE)); - } - - // make sure the name is in the list of required config variables - if(in_array($name,$reqConfigVars)){ - - - if(is_array($values)){ - throw new Exception('Configuration parameters must not contain arrays. The value for parameter "'.$name.'" is an array.'); - } - - $this->config[$name] = $values; - - }else{ - // unknown config variable, skipping - throw new Exception('Unknown configuration variable "'.$name.'"'); - } - - - } + // Now that we checked for BC, run the parent constructor + parent::__construct($config, $reserved); - $configVarsSet = array_keys($this->config); - foreach($reqConfigVars as $configVar){ - if(!in_array($configVar, $configVarsSet)){ - throw new Exception('Please provide a value for configuration parameter "'.$configVar.'".'); - } - } + // Get filter specific config options + $this->new_attribute = $this->config->getString('attribute.new'); + $this->search_attribute = $this->config->getString('search.attribute'); + $this->search_filter = $this->config->getString('search.filter'); } @@ -163,9 +102,10 @@ class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends SimpleSAML_Auth_Proc assert('array_key_exists("Attributes", $request)'); $attributes =& $request['Attributes']; + $map =& $this->attribute_map; - if(!isset($attributes[$this->config['userid_attribute']])){ - throw new Exception('The user\'s identity does not have an attribute called "'.$this->config['userid_attribute'].'"'); + if(!isset($attributes[$map['username']])){ + throw new Exception('The user\'s identity does not have an attribute called "'.$map['username'].'"'); } @@ -183,53 +123,23 @@ class sspmod_ldap_Auth_Process_AttributeAddFromLDAP extends SimpleSAML_Auth_Proc } // merge the attributes into the ldap_search_filter - $merged_ldap_search_filter = str_replace($arrSearch, $arrReplace, $this->config['ldap_search_filter']); - - - // connect to the LDAP directory - $ds = ldap_connect($this->config['ldap_host'], $this->config['ldap_port']); - - if(!$ds){ - throw new Exception('Failed to initialize LDAP connection parameters ('.ldap_error(NULL).')'); - } - - ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); - - // if we're supposed to bind as a specified user - if((isset($this->config['ldap_bind_user']) && $this->config['ldap_bind_user']) && - (isset($this->config['ldap_bind_pwd']) && $this->config['ldap_bind_pwd'])){ - - // bind to the directory as the specified user - if(!ldap_bind($ds, $this->config['ldap_bind_user'], $this->config['ldap_bind_pwd'])){ - throw new Exception($this->config['ldap_bind_user'].' failed to bind against '.$this->config['ldap_host'].' ('.ldap_error($ds).')'); - } - - }else{ // bind to the directory anonymously - - if(!ldap_bind($ds)){ - throw new Exception('Failed to anonymously bind against '.$this->config['ldap_host'].' ('.ldap_error($ds).')'); - } - - } + $filter = str_replace($arrSearch, $arrReplace, $this->search_filter); // search for matching entries - $sr = ldap_search($ds, $this->config['ldap_search_base_dn'], $merged_ldap_search_filter, array($this->config['ldap_search_attribute'])); - $entries = ldap_get_entries($ds, $sr); + $entries = $this->getLdap()->searchformultiple($this->base_dn, $filter, (array) $this->search_attribute, TRUE, FALSE); // handle [multiple] values if(is_array($entries) && is_array($entries[0])){ $results = array(); foreach($entries as $entry){ - $entry = $entry[strtolower($this->config['ldap_search_attribute'])]; + $entry = $entry[strtolower($this->search_attribute)]; for($i = 0; $i < $entry['count']; $i++){ $results[] = $entry[$i]; } } - $attributes[$this->config['new_attribute_name']] = array_values($results); + $attributes[$this->new_attribute] = array_values($results); } - ldap_unbind($ds); - } } diff --git a/modules/ldap/lib/Auth/Process/AttributeAddUsersGroups.php b/modules/ldap/lib/Auth/Process/AttributeAddUsersGroups.php new file mode 100644 index 000000000..06872b140 --- /dev/null +++ b/modules/ldap/lib/Auth/Process/AttributeAddUsersGroups.php @@ -0,0 +1,295 @@ +<?php + +/** + * Does a reverse membership lookup on the logged in user, + * looking for groups it is a member of and adds them to + * a defined attribute, in DN format. + * + * @author Ryan Panning <panman@traileyes.com> + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_ldap_Auth_Process_AttributeAddUsersGroups extends sspmod_ldap_Auth_Process_BaseFilter { + + + /** + * This is run when the filter is processed by SimpleSAML. + * It will attempt to find the current users groups using + * the best method possible for the LDAP product. The groups + * are then added to the request attributes. + * + * @throws SimpleSAML_Error_Exception + * @param $request + */ + public function process(&$request) { + assert('is_array($request)'); + assert('array_key_exists("Attributes", $request)'); + + // Log the process + SimpleSAML_Logger::debug( + $this->title . 'Attempting to get the users groups...' + ); + + // Reference the attributes, just to make the names shorter + $attributes =& $request['Attributes']; + $map =& $this->attribute_map; + + // Get the users groups from LDAP + $groups = $this->getGroups($attributes); + + // Make the array if it is not set already + if (!isset($attributes[$map['groups']])) { + $attributes[$map['groups']] = array(); + } + + // Must be an array, else cannot merge groups + if (!is_array($attributes[$map['groups']])) { + throw new SimpleSAML_Error_Exception( + $this->title . 'The group attribute [' . $map['groups'] . + '] is not an array of group DNs. ' . $this->var_export($attributes[$map['groups']]) + ); + } + + // Add the users group(s) + $group_attribute =& $attributes[$map['groups']]; + $group_attribute = array_merge($group_attribute, $groups); + $group_attribute = array_unique($group_attribute); + + // All done + SimpleSAML_Logger::debug( + $this->title . 'Added users groups to the group attribute [' . + $map['groups'] . ']: ' . implode('; ', $groups) + ); + } + + + /** + * This section of code was broken out because the child + * filter AuthorizeByGroup can use this method as well. + * Based on the LDAP product, it will do an optimized search + * using the required attribute values from the user to + * get their group membership, recursively. + * + * @throws SimpleSAML_Error_Exception + * @param array $attributes + * @return array + */ + protected function getGroups(array $attributes) { + + // Reference the map, just to make the name shorter + $map =& $this->attribute_map; + + // Log the request + SimpleSAML_Logger::debug( + $this->title . 'Checking for groups based on the best method for the LDAP product.' + ); + + // Based on the directory service, search LDAP for groups. + // If any attributes are needed, prepare them before calling search method + switch ($this->product) { + + case 'ACTIVEDIRECTORY': + + // Log the AD specific search + SimpleSAML_Logger::debug( + $this->title . 'Searching LDAP using ActiveDirectory specific method.' + ); + + // Make sure the defined dn attribute exists + if (!isset($attributes[$map['dn']])) { + throw new SimpleSAML_Error_Exception( + $this->title . 'The DN attribute [' . $map['dn'] . + '] is not defined in the users Attributes: ' . implode(', ', array_keys($attributes)) + ); + } + + // DN attribute must have a value + if (!isset($attributes[$map['dn']][0]) || !$attributes[$map['dn']][0]) { + throw new SimpleSAML_Error_Exception( + $this->title . 'The DN attribute [' . $map['dn'] . + '] does not have a [0] value defined. ' . $this->var_export($attributes[$map['dn']]) + ); + } + + // Pass to the AD specific search + $groups = $this->searchActiveDirectory($attributes[$map['dn']][0]); + break; + + default: + + // Log the general search + SimpleSAML_Logger::debug( + $this->title . 'Searching LDAP using the default search method.' + ); + + // Make sure the defined memberOf attribute exists + if (!isset($attributes[$map['memberof']])) { + throw new SimpleSAML_Error_Exception( + $this->title . 'The memberof attribute [' . $map['memberof'] . + '] is not defined in the users Attributes: ' . implode(', ', array_keys($attributes)) + ); + } + + // MemberOf must be an array of group DN's + if (!is_array($attributes[$map['memberof']])) { + throw new SimpleSAML_Error_Exception( + $this->title . 'The memberof attribute [' . $map['memberof'] . + '] is not an array of group DNs. ' . $this->var_export($attributes[$map['memberof']]) + ); + } + + // Search for the users group membership, recursively + $groups = $this->search($attributes[$map['memberof']]); + } + + // All done + SimpleSAML_Logger::debug( + $this->title . 'User found to be a member of the groups:' . implode('; ', $groups) + ); + return $groups; + } + + + /** + * Looks for groups from the list of DN's passed. Also + * recursively searches groups for further membership. + * Avoids loops by only searching a DN once. Returns + * the list of groups found. + * + * @param array $memberof + * @return array + */ + protected function search($memberof) { + assert('is_array($memberof)'); + + // Used to determine what DN's have already been searched + static $searched = array(); + + // Init the groups variable + $groups = array(); + + // Shorten the variable name + $map =& $this->attribute_map; + + // Log the search + SimpleSAML_Logger::debug( + $this->title . 'Checking DNs for groups.' . + ' DNs: '. implode('; ', $memberof) . + ' Attributes: ' . $map['memberof'] . ', ' . $map['type'] . + ' Group Type: ' . $this->type_map['group'] + ); + + // Check each DN of the passed memberOf + foreach ($memberof as $dn) { + + // Avoid infinite loops, only need to check a DN once + if (isset($searched[$dn])) { + continue; + } + + // Track all DN's that are searched + // Use DN for key as well, isset() is faster than in_array() + $searched[$dn] = $dn; + + // Query LDAP for the attribute values for the DN + try { + $attributes = $this->getLdap()->getAttributes($dn, array($map['memberof'], $map['type'])); + } catch (SimpleSAML_Error_AuthSource $e) { + continue; // DN must not exist, just continue. Logged by the LDAP object + } + + // Only look for groups + if (!in_array($this->type_map['group'], $attributes[$map['type']])) { + continue; + } + + // Add to found groups array + $groups[] = $dn; + + // Recursively search "sub" groups + $groups = array_merge($groups, $this->search($attributes[$map['memberof']])); + } + + // Return only the unique group names + return array_unique($groups); + } + + + /** + * Searches LDAP using a ActiveDirectory specific filter, + * looking for group membership for the users DN. Returns + * the list of group DNs retrieved. + * + * @param string $dn + * @return array + */ + protected function searchActiveDirectory($dn) { + assert('is_string($dn) && $dn != ""'); + + // Shorten the variable name + $map =& $this->attribute_map; + + // Log the search + SimpleSAML_Logger::debug( + $this->title . 'Searching ActiveDirectory group membership.' . + ' DN: ' . $dn . + ' DN Attribute: ' . $map['dn'] . + ' Member Attribute: ' . $map['member'] . + ' Type Attribute: ' . $map['type'] . + ' Type Value: ' . $this->type_map['group'] . + ' Base: ' . implode('; ', $this->base_dn) + ); + + // AD connections should have this set + $this->getLdap()->setOption(LDAP_OPT_REFERRALS, 0); + + // Search AD with the specific recursive flag + try { + $entries = $this->getLdap()->searchformultiple( + $this->base_dn, + array($map['type'] => $this->type_map['group'], $map['member'] . ':1.2.840.113556.1.4.1941:' => $dn), + array($map['dn']) + ); + + // The search may throw an exception if no entries + // are found, unlikely but possible. + } catch (SimpleSAML_Error_UserNotFound $e) { + return array(); + } + + //Init the groups + $groups = array(); + + // Check each entry.. + foreach ($entries as $entry) { + + // Check for the DN using the original attribute name + if (isset($entry[$map['dn']][0])) { + $groups[] = $entry[$map['dn']][0]; + continue; + } + + // Sometimes the returned attribute names are lowercase + if (isset($entry[strtolower($map['dn'])][0])) { + $groups[] = $entry[strtolower($map['dn'])][0]; + continue; + } + + // AD queries also seem to return the objects dn by default + if (isset($entry['dn'])) { + $groups[] = $entry['dn']; + continue; + } + + // Could not find DN, log and continue + SimpleSAML_Logger::notice( + $this->title . 'The DN attribute [' . + implode(', ', array($map['dn'], strtolower($map['dn']), 'dn')) . + '] could not be found in the entry. ' . $this->var_export($entry) + ); + } + + // All done + return $groups; + } +} diff --git a/modules/ldap/lib/Auth/Process/BaseFilter.php b/modules/ldap/lib/Auth/Process/BaseFilter.php new file mode 100644 index 000000000..0e92aa820 --- /dev/null +++ b/modules/ldap/lib/Auth/Process/BaseFilter.php @@ -0,0 +1,277 @@ +<?php + +/** + * This base LDAP filter class can be extended to enable real + * filter classes direct access to the authsource ldap config + * and connects to the ldap server. + * + * @author Ryan Panning <panman@traileyes.com> + * @package simpleSAMLphp + * @version $Id$ + */ +abstract class sspmod_ldap_Auth_Process_BaseFilter extends SimpleSAML_Auth_ProcessingFilter { + + /** + * List of attribute "alias's" linked to the real attribute + * name. Used for abstraction / configuration of the LDAP + * attribute names, which may change between dir service. + * + * @var array + */ + protected $attribute_map; + + + /** + * The base DN of the LDAP connection. Used when searching + * the LDAP server. + * + * @var string|array + */ + protected $base_dn; + + + /** + * The construct method will change the filter config into + * a SimpleSAML_Configuration object and store it here for + * later use, if needed. + * + * @var SimpleSAML_Configuration + */ + protected $config; + + + /** + * Instance, object of the ldap connection. Stored here to + * be access later during processing. + * + * @var sspmod_ldap_LdapConnection + */ + private $ldap; + + + /** + * Many times a LDAP product specific query can be used to + * speed up or reduce the filter process. This helps the + * child classes determine the product used to optimize + * those queries. + * + * @var string + */ + protected $product; + + + /** + * The class "title" used in logging and exception messages. + * This should be prepended to the beginning of the message. + * + * @var string + */ + protected $title = 'ldap:BaseFilter : '; + + + /** + * List of LDAP object types, used to determine the type of + * object that a DN references. + * + * @var array + */ + protected $type_map; + + + /** + * Checks the authsource, if defined, for configuration values + * to the LDAP server. Then sets up the LDAP connection for the + * instance/object and stores everything in class members. + * + * @throws SimpleSAML_Error_Exception + * @param array $config + * @param $reserved + */ + public function __construct(&$config, $reserved) { + parent::__construct($config, $reserved); + + // Change the class $title to match it's true name + // This way if the class is extended the proper name is used + $classname = get_class($this); + $classname = explode('_', $classname); + $this->title = 'ldap:' . end($classname) . ' : '; + + // Log the construction + SimpleSAML_Logger::debug( + $this->title . 'Creating and configuring the filter.' + ); + + // If an authsource was defined (an not empty string)... + if (isset($config['authsource']) && $config['authsource']) { + + // Log the authsource request + SimpleSAML_Logger::debug( + $this->title . 'Attempting to get configuration values from authsource [' . + $config['authsource'] . ']' + ); + + // Get the authsources file, which should contain the config + $authsource = SimpleSAML_Configuration::getConfig('authsources.php'); + + // Verify that the authsource config exists + if (!$authsource->hasValue($config['authsource'])) { + throw new SimpleSAML_Error_Exception( + $this->title . 'Authsource [' . $config['authsource'] . + '] defined in filter parameters not found in authsources.php' + ); + } + + // Get just the specified authsource config values + $authsource = $authsource->getConfigItem($config['authsource']); + $authsource = $authsource->toArray(); + + // Make sure it is an ldap source + // TODO: Support ldap:LDAPMulti, if possible + if (@$authsource[0] != 'ldap:LDAP') { + throw new SimpleSAML_Error_Exception( + $this->title . 'Authsource [' . $config['authsource'] . + '] specified in filter parameters is not an ldap:LDAP type' + ); + } + + // Build the authsource config + $authconfig = array(); + $authconfig['ldap.hostname'] = @$authsource['hostname']; + $authconfig['ldap.enable_tls'] = @$authsource['enable_tls']; + $authconfig['ldap.timeout'] = @$authsource['timeout']; + $authconfig['ldap.debug'] = @$authsource['debug']; + $authconfig['ldap.basedn'] = (@$authsource['search.enable'] ? @$authsource['search.base'] : NULL); + $authconfig['ldap.username'] = (@$authsource['search.enable'] ? @$authsource['search.username'] : NULL); + $authconfig['ldap.password'] = (@$authsource['search.enable'] ? @$authsource['search.password'] : NULL); + $authconfig['ldap.username'] = (@$authsource['priv.read'] ? @$authsource['priv.username'] : $authconfig['ldap.username']); + $authconfig['ldap.password'] = (@$authsource['priv.read'] ? @$authsource['priv.password'] : $authconfig['ldap.password']); + + // Only set the username attribute if the authsource specifies one attribute + if (@$authsource['search.enable'] && is_array(@$authsource['search.attributes']) + && count($authsource['search.attributes']) == 1) { + $authconfig['attribute.username'] = reset($authsource['search.attributes']); + } + + // Merge the authsource config with the filter config, + // but have the filter config override the authsource config + $config = array_merge($authconfig, $config); + + // Authsource complete + SimpleSAML_Logger::debug( + $this->title . 'Retrieved authsource [' . $config['authsource'] . + '] configuration values: ' . $this->var_export($authconfig) + ); + } + + // Convert the config array to a config class, + // that way we can verify type and define defaults. + // Store in the instance in-case needed later, by a child class. + $this->config = SimpleSAML_Configuration::loadFromArray($config, 'ldap:AuthProcess'); + + // Set all the filter values, setting defaults if needed + $this->base_dn = $this->config->getArrayizeString('ldap.basedn', ''); + $this->product = $this->config->getString('ldap.product', ''); + + // Cleanup the directory service, so that it is easier for + // child classes to determine service name consistently + $this->product = trim($this->product); + $this->product = strtoupper($this->product); + + // Log the member values retrieved above + SimpleSAML_Logger::debug( + $this->title . 'Configuration values retrieved;' . + ' BaseDN: ' . $this->var_export($this->base_dn) . + ' Product: ' . $this->var_export($this->product) + ); + + // Setup the attribute map which will be used to search LDAP + $this->attribute_map = array( + 'dn' => $this->config->getString('attribute.dn', 'distinguishedName'), + 'groups' => $this->config->getString('attribute.groups', 'groups'), + 'member' => $this->config->getString('attribute.member', 'member'), + 'memberof' => $this->config->getString('attribute.memberof', 'memberOf'), + 'name' => $this->config->getString('attribute.groupname', 'name'), + 'type' => $this->config->getString('attribute.type', 'objectClass'), + 'username' => $this->config->getString('attribute.username', 'sAMAccountName') + ); + + // Log the attribute map + SimpleSAML_Logger::debug( + $this->title . 'Attribute map created: ' . $this->var_export($this->attribute_map) + ); + + // Setup the object type map which is used to determine a DNs' type + $this->type_map = array( + 'group' => $this->config->getString('type.group', 'group'), + 'user' => $this->config->getString('type.user', 'user') + ); + + // Log the type map + SimpleSAML_Logger::debug( + $this->title . 'Type map created: ' . $this->var_export($this->type_map) + ); + } + + + /** + * Getter for the LDAP connection object. Created this getter + * rather than setting in the constructor to avoid unnecessarily + * connecting to LDAP when it might not be needed. + * + * @return sspmod_ldap_LdapConnection + */ + protected function getLdap() { + + // Check if already connected + if ($this->ldap) { + return $this->ldap; + } + + // Get the connection specific options + $hostname = $this->config->getString('ldap.hostname'); + $port = $this->config->getInteger('ldap.port', 389); + $enable_tls = $this->config->getBoolean('ldap.enable_tls', FALSE); + $debug = $this->config->getBoolean('ldap.debug', FALSE); + $timeout = $this->config->getInteger('ldap.timeout', 0); + $username = $this->config->getString('ldap.username', NULL); + $password = $this->config->getString('ldap.password', NULL); + + // Log the LDAP connection + SimpleSAML_Logger::debug( + $this->title . 'Connecting to LDAP server;' . + ' Hostname: ' . $hostname . + ' Port: ' . $port . + ' Enable TLS: ' . ($enable_tls ? 'Yes' : 'No') . + ' Debug: ' . ($debug ? 'Yes' : 'No') . + ' Timeout: ' . $timeout . + ' Username: ' . $username . + ' Password: ' . str_repeat('*', strlen($password)) + ); + + // Connect to the LDAP server to be queried during processing + $this->ldap = new SimpleSAML_Auth_LDAP($hostname, $enable_tls, $debug, $timeout, $port); + $this->ldap->bind($username, $password); + + // All done + return $this->ldap; + } + + + /** + * Local utility function to get details about a variable, + * basically converting it to a string to be used in a log + * message. The var_export() function returns several lines + * so this will remove the new lines and trim each line. + * + * @param mixed $value + * @return string + */ + protected function var_export($value) { + $export = var_export($value, TRUE); + $lines = explode("\n", $export); + foreach ($lines as &$line) { + $line = trim($line); + } + return implode(' ', $lines); + } +} -- GitLab