Merge branch 'feature/allow-to-list-groups-from-a-ldap-backend-9772'

resolves #9772
fixes #9950
This commit is contained in:
Johannes Meyer 2015-09-29 12:33:36 +02:00
commit f6e67670e6
5 changed files with 267 additions and 118 deletions

View File

@ -2,6 +2,7 @@
use Icinga\Data\Extensible; use Icinga\Data\Extensible;
use Icinga\Data\Updatable; use Icinga\Data\Updatable;
use Icinga\Data\Selectable;
$extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible; $extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible;
@ -67,7 +68,22 @@ foreach ($members as $member): ?>
<tbody> <tbody>
<?php endif ?> <?php endif ?>
<tr> <tr>
<td class="member-name"><?= $this->escape($member->user_name); ?></td> <td class="member-name">
<?php if (
$this->hasPermission('config/authentication/users/show')
&& ($userBackend = $backend->getUserBackend()) !== null
&& $userBackend instanceof Selectable
): ?>
<?= $this->qlink($member->user_name, 'user/show', array(
'backend' => $userBackend->getName(),
'user' => $member->user_name
), array(
'title' => sprintf($this->translate('Show detailed information about %s'), $member->user_name)
)); ?>
<?php else: ?>
<?= $this->escape($member->user_name); ?>
<?php endif ?>
</td>
<?php if (isset($removeForm)): ?> <?php if (isset($removeForm)): ?>
<td class="member-remove" data-base-target="_self"> <td class="member-remove" data-base-target="_self">
<?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?> <?php $removeForm->getElement('user_name')->setValue($member->user_name); echo $removeForm; ?>

View File

@ -12,10 +12,16 @@ use Icinga\Protocol\Ldap\Expression;
use Icinga\Repository\LdapRepository; use Icinga\Repository\LdapRepository;
use Icinga\Repository\RepositoryQuery; use Icinga\Repository\RepositoryQuery;
use Icinga\User; use Icinga\User;
use Icinga\Application\Logger;
class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBackendInterface class LdapUserGroupBackend extends LdapRepository implements UserGroupBackendInterface
{ {
/**
* The user backend being associated with this user group backend
*
* @var LdapUserBackend
*/
protected $userBackend;
/** /**
* The base DN to use for a user query * The base DN to use for a user query
* *
@ -105,84 +111,26 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
); );
/** /**
* Normed attribute names based on known LDAP environments * Set the user backend to be associated with this user group backend
* *
* @var array * @param LdapUserBackend $backend
*/
protected $normedAttributes = array(
'uid' => 'uid',
'gid' => 'gid',
'user' => 'user',
'group' => 'group',
'member' => 'member',
'inetorgperson' => 'inetOrgPerson',
'samaccountname' => 'sAMAccountName'
);
/**
* The name of this repository
*
* @var string
*/
protected $name;
/**
* The datasource being used
*
* @var Connection
*/
protected $ds;
/**
* Create a new LDAP repository object
*
* @param Connection $ds The data source to use
*/
public function __construct($ds)
{
$this->ds = $ds;
}
/**
* Return the given attribute name normed to known LDAP enviroments, if possible
*
* @param string $name
*
* @return string
*/
protected function getNormedAttribute($name)
{
$loweredName = strtolower($name);
if (array_key_exists($loweredName, $this->normedAttributes)) {
return $this->normedAttributes[$loweredName];
}
return $name;
}
/**
* Set this repository's name
*
* @param string $name
* *
* @return $this * @return $this
*/ */
public function setName($name) public function setUserBackend(LdapUserBackend $backend)
{ {
$this->name = $name; $this->userBackend = $backend;
return $this; return $this;
} }
/** /**
* Return this repository's name * Return the user backend being associated with this user group backend
* *
* In case no name has been explicitly set yet, the class name is returned. * @return LdapUserBackend
*
* @return string
*/ */
public function getName() public function getUserBackend()
{ {
return $this->name; return $this->userBackend;
} }
/** /**
@ -453,7 +401,6 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
$lastModifiedAttribute = 'modifyTimestamp'; $lastModifiedAttribute = 'modifyTimestamp';
} }
// TODO(jom): Fetching memberships does not work currently, we'll need some aggregate functionality!
$columns = array( $columns = array(
'group' => $this->groupNameAttribute, 'group' => $this->groupNameAttribute,
'group_name' => $this->groupNameAttribute, 'group_name' => $this->groupNameAttribute,
@ -492,13 +439,37 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
if ($this->groupClass === null) { if ($this->groupClass === null) {
throw new ProgrammingError('It is required to set the objectClass where to look for groups first'); 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');
}
return array( $rules = array(
$this->groupClass => array( $this->groupClass => array(
'created_at' => 'generalized_time', 'created_at' => 'generalized_time',
'last_modified' => 'generalized_time' 'last_modified' => 'generalized_time'
) )
); );
if (! $this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) {
$rules[$this->groupClass][] = 'user_name';
}
return $rules;
}
/**
* Return the uid for the given distinguished name
*
* @param string $username
*
* @param string
*/
protected function retrieveUserName($dn)
{
return $this->ds
->select()
->from('*', array($this->userNameAttribute))
->setBase($dn)
->fetchOne();
} }
/** /**
@ -524,6 +495,27 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
return $table; return $table;
} }
/**
* Validate that the given column is a valid query target and return it or the actual name if it's an alias
*
* @param string $table The table where to look for the column or alias
* @param string $name The name or alias of the column to validate
* @param RepositoryQuery $query An optional query to pass as context
*
* @return string The given column's name
*
* @throws QueryException In case the given column is not a valid query column
*/
public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
{
$column = parent::requireQueryColumn($table, $name, $query);
if ($name === 'user_name' && $query !== null) {
$query->getQuery()->setUnfoldAttribute('user_name');
}
return $column;
}
/** /**
* Return the groups the given user is a member of * Return the groups the given user is a member of
* *
@ -533,43 +525,37 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
*/ */
public function getMemberships(User $user) public function getMemberships(User $user)
{ {
if ($this->groupClass === 'posixGroup') { if ($this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) {
// Posix group only uses simple user name $queryValue = $user->getUsername();
$userDn = $user->getUsername(); } elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) {
} else { $userQuery = $this->ds
// LDAP groups use the complete DN ->select()
if (($userDn = $user->getAdditional('ldap_dn')) === null) { ->from($this->userClass)
$userQuery = $this->ds ->where($this->userNameAttribute, $user->getUsername())
->select() ->setBase($this->userBaseDn)
->from($this->userClass) ->setUsePagedResults(false);
->where($this->userNameAttribute, $user->getUsername()) if ($this->userFilter) {
->setBase($this->userBaseDn) $userQuery->where(new Expression($this->userFilter));
->setUsePagedResults(false); }
if ($this->userFilter) {
$userQuery->where(new Expression($this->userFilter));
}
if (($userDn = $userQuery->fetchDn()) === null) { if (($queryValue = $userQuery->fetchDn()) === null) {
return array(); return array();
}
} }
} }
$groupQuery = $this->ds $groupQuery = $this->ds
->select() ->select()
->from($this->groupClass, array($this->groupNameAttribute)) ->from($this->groupClass, array($this->groupNameAttribute))
->where($this->groupMemberAttribute, $userDn) ->where($this->groupMemberAttribute, $queryValue)
->setBase($this->groupBaseDn); ->setBase($this->groupBaseDn);
if ($this->groupFilter) { if ($this->groupFilter) {
$groupQuery->where(new Expression($this->groupFilter)); $groupQuery->where(new Expression($this->groupFilter));
} }
Logger::debug('Fetching groups for user %s using filter %s.', $user->getUsername(), $groupQuery->__toString());
$groups = array(); $groups = array();
foreach ($groupQuery as $row) { foreach ($groupQuery as $row) {
$groups[] = $row->{$this->groupNameAttribute}; $groups[] = $row->{$this->groupNameAttribute};
} }
Logger::debug('Fetched %d groups: %s.', count($groups), join(', ', $groups));
return $groups; return $groups;
} }
@ -610,6 +596,7 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
); );
} }
$this->setUserBackend($userBackend);
$defaults->merge(array( $defaults->merge(array(
'user_base_dn' => $userBackend->getBaseDn(), 'user_base_dn' => $userBackend->getBaseDn(),
'user_class' => $userBackend->getUserClass(), 'user_class' => $userBackend->getUserClass(),

View File

@ -358,9 +358,25 @@ class LdapConnection implements Selectable, Inspectable
*/ */
public function count(LdapQuery $query) public function count(LdapQuery $query)
{ {
$ds = $this->getConnection();
$this->bind(); $this->bind();
if (($unfoldAttribute = $query->getUnfoldAttribute()) !== null) {
$desiredColumns = $query->getColumns();
if (isset($desiredColumns[$unfoldAttribute])) {
$fields = array($unfoldAttribute => $desiredColumns[$unfoldAttribute]);
} elseif (in_array($unfoldAttribute, $desiredColumns, true)) {
$fields = array($unfoldAttribute);
} else {
throw new ProgrammingError(
'The attribute used to unfold a query\'s result must be selected'
);
}
$res = $this->runQuery($query, $fields);
return count($res);
}
$ds = $this->getConnection();
$results = @ldap_search( $results = @ldap_search(
$ds, $ds,
$query->getBase() ?: $this->getDn(), $query->getBase() ?: $this->getDn(),
@ -658,7 +674,7 @@ class LdapConnection implements Selectable, Inspectable
protected function runQuery(LdapQuery $query, array $fields = null) protected function runQuery(LdapQuery $query, array $fields = null)
{ {
$limit = $query->getLimit(); $limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; $offset = $query->hasOffset() ? $query->getOffset() : 0;
if ($fields === null) { if ($fields === null) {
$fields = $query->getColumns(); $fields = $query->getColumns();
@ -711,13 +727,41 @@ class LdapConnection implements Selectable, Inspectable
$count = 0; $count = 0;
$entries = array(); $entries = array();
$entry = ldap_first_entry($ds, $results); $entry = ldap_first_entry($ds, $results);
$unfoldAttribute = $query->getUnfoldAttribute();
do { do {
$count += 1; if ($unfoldAttribute) {
if (! $serverSorting || $offset === 0 || $offset < $count) { $rows = $this->cleanupAttributes(
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry), ldap_get_attributes($ds, $entry),
array_flip($fields) array_flip($fields),
$unfoldAttribute
); );
if (is_array($rows)) {
// TODO: Register the DN the same way as a section name in the ArrayDatasource!
foreach ($rows as $row) {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[] = $row;
}
if ($serverSorting && $limit > 0 && $limit === count($entries)) {
break;
}
}
} else {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($ds, $entry)] = $rows;
}
}
} else {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry),
array_flip($fields)
);
}
} }
} while ((! $serverSorting || $limit === 0 || $limit !== count($entries)) } while ((! $serverSorting || $limit === 0 || $limit !== count($entries))
&& ($entry = ldap_next_entry($ds, $entry)) && ($entry = ldap_next_entry($ds, $entry))
@ -754,7 +798,7 @@ class LdapConnection implements Selectable, Inspectable
} }
$limit = $query->getLimit(); $limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0; $offset = $query->hasOffset() ? $query->getOffset() : 0;
$queryString = (string) $query; $queryString = (string) $query;
$base = $query->getBase() ?: $this->rootDn; $base = $query->getBase() ?: $this->rootDn;
@ -776,6 +820,7 @@ class LdapConnection implements Selectable, Inspectable
$count = 0; $count = 0;
$cookie = ''; $cookie = '';
$entries = array(); $entries = array();
$unfoldAttribute = $query->getUnfoldAttribute();
do { do {
// Do not request the pagination control as a critical extension, as we want the // Do not request the pagination control as a critical extension, as we want the
// server to return results even if the paged search request cannot be satisfied // server to return results even if the paged search request cannot be satisfied
@ -826,12 +871,39 @@ class LdapConnection implements Selectable, Inspectable
$entry = ldap_first_entry($ds, $results); $entry = ldap_first_entry($ds, $results);
do { do {
$count += 1; if ($unfoldAttribute) {
if (! $serverSorting || $offset === 0 || $offset < $count) { $rows = $this->cleanupAttributes(
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry), ldap_get_attributes($ds, $entry),
array_flip($fields) array_flip($fields),
$unfoldAttribute
); );
if (is_array($rows)) {
// TODO: Register the DN the same way as a section name in the ArrayDatasource!
foreach ($rows as $row) {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[] = $row;
}
if ($serverSorting && $limit > 0 && $limit === count($entries)) {
break;
}
}
} else {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($ds, $entry)] = $rows;
}
}
} else {
$count += 1;
if (! $serverSorting || $offset === 0 || $offset < $count) {
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry),
array_flip($fields)
);
}
} }
} while ( } while (
(! $serverSorting || $limit === 0 || $limit !== count($entries)) (! $serverSorting || $limit === 0 || $limit !== count($entries))
@ -861,9 +933,6 @@ class LdapConnection implements Selectable, Inspectable
// the server: https://www.ietf.org/rfc/rfc2696.txt // the server: https://www.ietf.org/rfc/rfc2696.txt
ldap_control_paged_result($ds, 0, false, $cookie); ldap_control_paged_result($ds, 0, false, $cookie);
ldap_search($ds, $base, $queryString); // Returns no entries, due to the page size ldap_search($ds, $base, $queryString); // Returns no entries, due to the page size
} else {
// Reset the paged search request so that subsequent requests succeed
ldap_control_paged_result($ds, 0);
} }
if (! $serverSorting && $query->hasOrder()) { if (! $serverSorting && $query->hasOrder()) {
@ -879,14 +948,16 @@ class LdapConnection implements Selectable, Inspectable
/** /**
* Clean up the given attributes and return them as simple object * Clean up the given attributes and return them as simple object
* *
* Applies column aliases, aggregates multi-value attributes as array and sets null for each missing attribute. * Applies column aliases, aggregates/unfolds multi-value attributes
* as array and sets null for each missing attribute.
* *
* @param array $attributes * @param array $attributes
* @param array $requestedFields * @param array $requestedFields
* @param string $unfoldAttribute
* *
* @return object * @return object|array An array in case the object has been unfolded
*/ */
public function cleanupAttributes($attributes, array $requestedFields) public function cleanupAttributes($attributes, array $requestedFields, $unfoldAttribute = null)
{ {
// In case the result contains attributes with a differing case than the requested fields, it is // 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. // necessary to create another array to map attributes case insensitively to their requested counterparts.
@ -927,6 +998,24 @@ class LdapConnection implements Selectable, Inspectable
} }
} }
if (
$unfoldAttribute !== null
&& isset($cleanedAttributes[$unfoldAttribute])
&& is_array($cleanedAttributes[$unfoldAttribute])
) {
$values = $cleanedAttributes[$unfoldAttribute];
unset($cleanedAttributes[$unfoldAttribute]);
$baseRow = (object) $cleanedAttributes;
$rows = array();
foreach ($values as $value) {
$row = clone $baseRow;
$row->{$unfoldAttribute} = $value;
$rows[] = $row;
}
return $rows;
}
return (object) $cleanedAttributes; return (object) $cleanedAttributes;
} }

View File

@ -35,6 +35,13 @@ class LdapQuery extends SimpleQuery
*/ */
protected $usePagedResults; protected $usePagedResults;
/**
* The name of the attribute used to unfold the result
*
* @var string
*/
protected $unfoldAttribute;
/** /**
* Initialize this query * Initialize this query
*/ */
@ -90,6 +97,29 @@ class LdapQuery extends SimpleQuery
return $this->usePagedResults; return $this->usePagedResults;
} }
/**
* Set the attribute to be used to unfold the result
*
* @param string $attributeName
*
* @return $this
*/
public function setUnfoldAttribute($attributeName)
{
$this->unfoldAttribute = $attributeName;
return $this;
}
/**
* Return the attribute to use to unfold the result
*
* @return string
*/
public function getUnfoldAttribute()
{
return $this->unfoldAttribute;
}
/** /**
* Choose an objectClass and the columns you are interested in * Choose an objectClass and the columns you are interested in
* *

View File

@ -28,13 +28,27 @@ abstract class LdapRepository extends Repository
* @var array * @var array
*/ */
protected $normedAttributes = array( protected $normedAttributes = array(
'uid' => 'uid', 'uid' => 'uid',
'gid' => 'gid', 'gid' => 'gid',
'user' => 'user', 'user' => 'user',
'group' => 'group', 'group' => 'group',
'member' => 'member', 'member' => 'member',
'inetorgperson' => 'inetOrgPerson', 'memberuid' => 'memberUid',
'samaccountname' => 'sAMAccountName' 'posixgroup' => 'posixGroup',
'uniquemember' => 'uniqueMember',
'groupofnames' => 'groupOfNames',
'inetorgperson' => 'inetOrgPerson',
'samaccountname' => 'sAMAccountName',
'groupofuniquenames' => 'groupOfUniqueNames'
);
/**
* Object attributes whose value is not distinguished name
*
* @var array
*/
protected $ambiguousAttributes = array(
'posixGroup' => 'memberUid'
); );
/** /**
@ -63,4 +77,17 @@ abstract class LdapRepository extends Repository
return $name; return $name;
} }
/**
* Return whether the given object attribute's value is not a distinguished name
*
* @param string $objectClass
* @param string $attributeName
*
* @return bool
*/
protected function isAmbiguous($objectClass, $attributeName)
{
return isset($this->ambiguousAttributes[$objectClass][$attributeName]);
}
} }