Merge branch 'bugfix/unreliable-attribute-ambiguity-check-10567'

fixes #10567
This commit is contained in:
Johannes Meyer 2015-11-11 13:10:49 +01:00
commit 1bfefe060e
7 changed files with 201 additions and 88 deletions

View File

@ -37,7 +37,16 @@ userpassword: password
dn: cn=Users,ou=groups,dc=icinga,dc=org
objectClass: groupOfUniqueNames
cn: Users
uniqueMember: cn=Jon Doe,ou=people,dc=icinga,dc=org
uniqueMember: cn=John Doe,ou=people,dc=icinga,dc=org
uniqueMember: cn=Jane Smith,ou=people,dc=icinga,dc=org
uniqueMember: cn=John Q. Public,ou=people,dc=icinga,dc=org
uniqueMember: cn=Richard Roe,ou=people,dc=icinga,dc=org
dn: cn=PosixUsers,ou=groups,dc=icinga,dc=org
objectClass: posixGroup
cn: PosixUsers
gidNumber: 2001
memberUid: jdoe
memberUid: jsmith
memberUid: jqpublic
memberUid: rroe

View File

@ -136,6 +136,16 @@ class Logger
return $this;
}
/**
* Return the logging level being used
*
* @return int
*/
public function getLevel()
{
return $this->level;
}
/**
* Register the given message as config error
*

View File

@ -190,24 +190,6 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
->setFilter($config->filter);
}
/**
* Return a new query for the given columns
*
* @param array $columns The desired columns, if null all columns will be queried
*
* @return RepositoryQuery
*/
public function select(array $columns = null)
{
$query = parent::select($columns);
$query->getQuery()->setBase($this->baseDn);
if ($this->filter) {
$query->getQuery()->setNativeFilter($this->filter);
}
return $query;
}
/**
* Initialize this repository's virtual tables
*
@ -335,6 +317,28 @@ class LdapUserBackend extends LdapRepository implements UserBackendInterface, In
return ((int) $value) >= $bigBang->diff($now)->days;
}
/**
* Validate that the requested table exists
*
* @param string $table The table to validate
* @param RepositoryQuery $query An optional query to pass as context
*
* @return string
*
* @throws ProgrammingError In case the given table does not exist
*/
public function requireTable($table, RepositoryQuery $query = null)
{
if ($query !== null) {
$query->getQuery()->setBase($this->baseDn);
if ($this->filter) {
$query->getQuery()->setNativeFilter($this->filter);
}
}
return parent::requireTable($table, $query);
}
/**
* Validate that the given column is a valid query target and return it or the actual name if it's an alias
*

View File

@ -72,6 +72,13 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*/
protected $groupMemberAttribute;
/**
* Whether the attribute name where to find a group's member holds ambiguous values
*
* @var bool
*/
protected $ambiguousMemberAttribute;
/**
* The custom LDAP filter to apply on a user query
*
@ -358,17 +365,36 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
}
/**
* Return a new query for the given columns
* Return whether the attribute name where to find a group's member holds ambiguous values
*
* @param array $columns The desired columns, if null all columns will be queried
* @return bool
*
* @return RepositoryQuery
* @throws ProgrammingError In case either $this->groupClass or $this->groupMemberAttribute
* has not been set yet
*/
public function select(array $columns = null)
protected function isMemberAttributeAmbiguous()
{
$query = parent::select($columns);
$query->getQuery()->setBase($this->groupBaseDn);
return $query;
if ($this->ambiguousMemberAttribute === null) {
if ($this->groupClass === null) {
throw new ProgrammingError(
'It is required to set the objectClass where to look for groups first'
);
} elseif ($this->groupMemberAttribute === null) {
throw new ProgrammingError(
'It is required to set a attribute name where to find a group\'s members first'
);
}
$sampleValue = $this->ds
->select()
->from($this->groupClass, array($this->groupMemberAttribute))
->setUnfoldAttribute($this->groupMemberAttribute)
->setBase($this->groupBaseDn)
->fetchOne();
$this->ambiguousMemberAttribute = !$this->isRelatedDn($sampleValue);
}
return $this->ambiguousMemberAttribute;
}
/**
@ -445,19 +471,9 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
* Initialize this repository's conversion rules
*
* @return array
*
* @throws ProgrammingError In case either $this->groupClass or $this->groupMemberAttribute
* has not been set yet
*/
protected function initializeConversionRules()
{
if ($this->groupClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for groups first');
}
if ($this->groupMemberAttribute === null) {
throw new ProgrammingError('It is required to set a attribute name where to find a group\'s members first');
}
$rules = array(
'group' => array(
'created_at' => 'generalized_time',
@ -468,7 +484,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
'last_modified' => 'generalized_time'
)
);
if (! $this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) {
if (! $this->isMemberAttributeAmbiguous()) {
$rules['group_membership']['user_name'] = 'user_name';
$rules['group_membership']['user'] = 'user_name';
$rules['group']['user_name'] = 'user_name';
@ -492,6 +508,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
->select()
->from($this->userClass, array())
->where($this->userNameAttribute, $name)
->setBase($this->userBaseDn)
->setUsePagedResults(false)
->fetchDn();
if ($userDn) {
@ -502,6 +519,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
->select()
->from($this->groupClass, array())
->where($this->groupNameAttribute, $name)
->setBase($this->groupBaseDn)
->setUsePagedResults(false)
->fetchDn();
if ($groupDn) {
@ -543,8 +561,11 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*/
public function requireTable($table, RepositoryQuery $query = null)
{
if ($query !== null && $table === 'group' && $this->groupFilter) {
$query->getQuery()->setNativeFilter($this->groupFilter);
if ($query !== null) {
$query->getQuery()->setBase($this->groupBaseDn);
if ($table === 'group' && $this->groupFilter) {
$query->getQuery()->setNativeFilter($this->groupFilter);
}
}
return parent::requireTable($table, $query);
@ -580,7 +601,7 @@ class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInt
*/
public function getMemberships(User $user)
{
if ($this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) {
if ($this->isMemberAttributeAmbiguous()) {
$queryValue = $user->getUsername();
} elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) {
$userQuery = $this->ds

View File

@ -378,14 +378,7 @@ class LdapConnection implements Selectable, Inspectable
}
$ds = $this->getConnection();
$results = @ldap_search(
$ds,
$query->getBase() ?: $this->getDn(),
(string) $query,
array('dn'),
0,
0
);
$results = $this->ldapSearch($query, array('dn'));
if ($results === false) {
if (ldap_errno($ds) !== self::LDAP_NO_SUCH_OBJECT) {
@ -483,8 +476,28 @@ class LdapConnection implements Selectable, Inspectable
*/
public function fetchOne(LdapQuery $query, array $fields = null)
{
$row = (array) $this->fetchRow($query, $fields);
return array_shift($row) ?: false;
$row = $this->fetchRow($query, $fields);
if ($row === false) {
return false;
}
$values = get_object_vars($row);
if (empty($values)) {
return false;
}
if ($fields === null) {
// Fetch the desired columns from the query if not explicitly overriden in the method's parameter
$fields = $query->getColumns();
}
if (empty($fields)) {
// The desired columns may be empty independently whether provided by the query or the method's parameter
return array_shift($values);
}
$alias = key($fields);
return $values[is_string($alias) ? $alias : $fields[$alias]];
}
/**
@ -712,14 +725,10 @@ class LdapConnection implements Selectable, Inspectable
}
}
$results = @ldap_search(
$ds,
$query->getBase() ?: $this->rootDn,
(string) $query,
$unfoldAttribute
? array_unique(array_values($fields))
: array_values($fields),
0, // Attributes and values
$results = $this->ldapSearch(
$query,
array_values($fields),
0,
$serverSorting && $limit ? $offset + $limit : 0
);
if ($results === false) {
@ -808,8 +817,6 @@ class LdapConnection implements Selectable, Inspectable
$limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() : 0;
$queryString = (string) $query;
$base = $query->getBase() ?: $this->rootDn;
if ($fields === null) {
$fields = $query->getColumns();
@ -853,14 +860,10 @@ class LdapConnection implements Selectable, Inspectable
));
}
$results = @ldap_search(
$ds,
$base,
$queryString,
$unfoldAttribute
? array_unique(array_values($fields))
: array_values($fields),
0, // Attributes and values
$results = $this->ldapSearch(
$query,
array_values($fields),
0,
$serverSorting && $limit ? $offset + $limit : 0
);
if ($results === false) {
@ -870,8 +873,8 @@ class LdapConnection implements Selectable, Inspectable
throw new LdapException(
'LDAP query "%s" (base %s) failed. Error: %s',
$queryString,
$base,
(string) $query,
$query->getBase() ?: $this->getDn(),
ldap_error($ds)
);
} elseif (ldap_count_entries($ds, $results) === 0) {
@ -950,7 +953,8 @@ class LdapConnection implements Selectable, Inspectable
// pagedResultsControl with the size set to zero (0) and the cookie set to the last cookie returned by
// the server: https://www.ietf.org/rfc/rfc2696.txt
ldap_control_paged_result($ds, 0, false, $cookie);
ldap_search($ds, $base, $queryString); // Returns no entries, due to the page size
// Returns no entries, due to the page size
ldap_search($ds, $query->getBase() ?: $this->getDn(), (string) $query);
}
if (! $serverSorting && $query->hasOrder()) {
@ -1164,6 +1168,77 @@ class LdapConnection implements Selectable, Inspectable
return $ds;
}
/**
* Perform a LDAP search and return the result
*
* @param LdapQuery $query
* @param array $attributes An array of the required attributes
* @param int $attrsonly Should be set to 1 if only attribute types are wanted
* @param int $sizelimit Enables you to limit the count of entries fetched
* @param int $timelimit Sets the number of seconds how long is spend on the search
* @param int $deref
*
* @return resource|bool A search result identifier or false on error
*/
public function ldapSearch(
LdapQuery $query,
array $attributes = null,
$attrsonly = 0,
$sizelimit = 0,
$timelimit = 0,
$deref = LDAP_DEREF_NEVER
) {
$queryString = (string) $query;
$baseDn = $query->getBase() ?: $this->getDn();
if (Logger::getInstance()->getLevel() === Logger::DEBUG) {
// We're checking the level by ourself to avoid rendering the ldapsearch commandline for nothing
$starttlsParam = $this->encryption === static::STARTTLS ? ' -ZZ' : '';
$ldapUrl = ($this->encryption === static::LDAPS ? 'ldaps://' : 'ldap://')
. $this->hostname
. ($this->port ? ':' . $this->port : '');
if ($this->bound) {
$bindParams = ' -D "' . $this->bindDn . '"' . ($this->bindPw ? ' -w "' . $this->bindPw . '"' : '');
}
if ($deref === LDAP_DEREF_NEVER) {
$derefName = 'never';
} elseif ($deref === LDAP_DEREF_ALWAYS) {
$derefName = 'always';
} elseif ($deref === LDAP_DEREF_SEARCHING) {
$derefName = 'search';
} else { // $deref === LDAP_DEREF_FINDING
$derefName = 'find';
}
Logger::debug("Issueing LDAP search. Use '%s' to reproduce.", sprintf(
'ldapsearch -P 3%s -H "%s"%s -b "%s" -s "sub" -z %u -l %u -a "%s"%s%s%s',
$starttlsParam,
$ldapUrl,
$bindParams,
$baseDn,
$sizelimit,
$timelimit,
$derefName,
$attrsonly ? ' -A' : '',
$queryString ? ' "' . $queryString . '"' : '',
$attributes ? ' "' . join('" "', $attributes) . '"' : ''
));
}
return @ldap_search(
$this->getConnection(),
$baseDn,
$queryString,
$attributes,
$attrsonly,
$sizelimit,
$timelimit,
$deref
);
}
/**
* Create an LDAP entry
*

View File

@ -42,15 +42,6 @@ abstract class LdapRepository extends Repository
'groupofuniquenames' => 'groupOfUniqueNames'
);
/**
* Object attributes whose value is not distinguished name
*
* @var array
*/
protected $ambiguousAttributes = array(
'posixGroup' => 'memberUid'
);
/**
* Create a new LDAP repository object
*
@ -79,15 +70,19 @@ abstract class LdapRepository extends Repository
}
/**
* Return whether the given object attribute's value is not a distinguished name
* Return whether the given object DN is related to the given base DN
*
* @param string $objectClass
* @param string $attributeName
* Will use the current connection's root DN if $baseDn is not given.
*
* @param string $dn The object DN to check
* @param string $baseDn The base DN to compare the object DN with
*
* @return bool
*/
protected function isAmbiguous($objectClass, $attributeName)
protected function isRelatedDn($dn, $baseDn = null)
{
return isset($this->ambiguousAttributes[$objectClass][$attributeName]);
$normalizedDn = strtolower(join(',', array_map('trim', explode(',', $dn))));
$normalizedBaseDn = strtolower(join(',', array_map('trim', explode(',', $baseDn ?: $this->ds->getDn()))));
return strpos($normalizedDn, $normalizedBaseDn) !== false;
}
}

View File

@ -86,9 +86,8 @@ class RepositoryQuery implements QueryInterface, SortRules, FilterColumns, Itera
*/
public function from($target, array $columns = null)
{
$this->query = $this->repository->getDataSource()->select()->from(
$this->repository->requireTable($target, $this)
);
$this->query = $this->repository->getDataSource()->select();
$this->query->from($this->repository->requireTable($target, $this));
$this->query->columns($this->prepareQueryColumns($target, $columns));
$this->target = $target;
return $this;