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\Updatable;
use Icinga\Data\Selectable;
$extensible = $this->hasPermission('config/authentication/groups/add') && $backend instanceof Extensible;
@ -67,7 +68,22 @@ foreach ($members as $member): ?>
<tbody>
<?php endif ?>
<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)): ?>
<td class="member-remove" data-base-target="_self">
<?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\RepositoryQuery;
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
*
@ -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
*/
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
* @param LdapUserBackend $backend
*
* @return $this
*/
public function setName($name)
public function setUserBackend(LdapUserBackend $backend)
{
$this->name = $name;
$this->userBackend = $backend;
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 string
* @return LdapUserBackend
*/
public function getName()
public function getUserBackend()
{
return $this->name;
return $this->userBackend;
}
/**
@ -453,7 +401,6 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
$lastModifiedAttribute = 'modifyTimestamp';
}
// TODO(jom): Fetching memberships does not work currently, we'll need some aggregate functionality!
$columns = array(
'group' => $this->groupNameAttribute,
'group_name' => $this->groupNameAttribute,
@ -492,13 +439,37 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
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');
}
return array(
$rules = array(
$this->groupClass => array(
'created_at' => '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;
}
/**
* 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
*
@ -533,12 +525,9 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
*/
public function getMemberships(User $user)
{
if ($this->groupClass === 'posixGroup') {
// Posix group only uses simple user name
$userDn = $user->getUsername();
} else {
// LDAP groups use the complete DN
if (($userDn = $user->getAdditional('ldap_dn')) === null) {
if ($this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) {
$queryValue = $user->getUsername();
} elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) {
$userQuery = $this->ds
->select()
->from($this->userClass)
@ -549,27 +538,24 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
$userQuery->where(new Expression($this->userFilter));
}
if (($userDn = $userQuery->fetchDn()) === null) {
if (($queryValue = $userQuery->fetchDn()) === null) {
return array();
}
}
}
$groupQuery = $this->ds
->select()
->from($this->groupClass, array($this->groupNameAttribute))
->where($this->groupMemberAttribute, $userDn)
->where($this->groupMemberAttribute, $queryValue)
->setBase($this->groupBaseDn);
if ($this->groupFilter) {
$groupQuery->where(new Expression($this->groupFilter));
}
Logger::debug('Fetching groups for user %s using filter %s.', $user->getUsername(), $groupQuery->__toString());
$groups = array();
foreach ($groupQuery as $row) {
$groups[] = $row->{$this->groupNameAttribute};
}
Logger::debug('Fetched %d groups: %s.', count($groups), join(', ', $groups));
return $groups;
}
@ -610,6 +596,7 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
);
}
$this->setUserBackend($userBackend);
$defaults->merge(array(
'user_base_dn' => $userBackend->getBaseDn(),
'user_class' => $userBackend->getUserClass(),

View File

@ -358,9 +358,25 @@ class LdapConnection implements Selectable, Inspectable
*/
public function count(LdapQuery $query)
{
$ds = $this->getConnection();
$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(
$ds,
$query->getBase() ?: $this->getDn(),
@ -658,7 +674,7 @@ class LdapConnection implements Selectable, Inspectable
protected function runQuery(LdapQuery $query, array $fields = null)
{
$limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
$offset = $query->hasOffset() ? $query->getOffset() : 0;
if ($fields === null) {
$fields = $query->getColumns();
@ -711,7 +727,34 @@ class LdapConnection implements Selectable, Inspectable
$count = 0;
$entries = array();
$entry = ldap_first_entry($ds, $results);
$unfoldAttribute = $query->getUnfoldAttribute();
do {
if ($unfoldAttribute) {
$rows = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry),
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(
@ -719,6 +762,7 @@ class LdapConnection implements Selectable, Inspectable
array_flip($fields)
);
}
}
} while ((! $serverSorting || $limit === 0 || $limit !== count($entries))
&& ($entry = ldap_next_entry($ds, $entry))
);
@ -754,7 +798,7 @@ class LdapConnection implements Selectable, Inspectable
}
$limit = $query->getLimit();
$offset = $query->hasOffset() ? $query->getOffset() - 1 : 0;
$offset = $query->hasOffset() ? $query->getOffset() : 0;
$queryString = (string) $query;
$base = $query->getBase() ?: $this->rootDn;
@ -776,6 +820,7 @@ class LdapConnection implements Selectable, Inspectable
$count = 0;
$cookie = '';
$entries = array();
$unfoldAttribute = $query->getUnfoldAttribute();
do {
// 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
@ -826,6 +871,32 @@ class LdapConnection implements Selectable, Inspectable
$entry = ldap_first_entry($ds, $results);
do {
if ($unfoldAttribute) {
$rows = $this->cleanupAttributes(
ldap_get_attributes($ds, $entry),
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(
@ -833,6 +904,7 @@ class LdapConnection implements Selectable, Inspectable
array_flip($fields)
);
}
}
} while (
(! $serverSorting || $limit === 0 || $limit !== count($entries))
&& ($entry = ldap_next_entry($ds, $entry))
@ -861,9 +933,6 @@ class LdapConnection implements Selectable, Inspectable
// 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
} else {
// Reset the paged search request so that subsequent requests succeed
ldap_control_paged_result($ds, 0);
}
if (! $serverSorting && $query->hasOrder()) {
@ -879,14 +948,16 @@ class LdapConnection implements Selectable, Inspectable
/**
* 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 $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
// 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;
}

View File

@ -35,6 +35,13 @@ class LdapQuery extends SimpleQuery
*/
protected $usePagedResults;
/**
* The name of the attribute used to unfold the result
*
* @var string
*/
protected $unfoldAttribute;
/**
* Initialize this query
*/
@ -90,6 +97,29 @@ class LdapQuery extends SimpleQuery
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
*

View File

@ -33,8 +33,22 @@ abstract class LdapRepository extends Repository
'user' => 'user',
'group' => 'group',
'member' => 'member',
'memberuid' => 'memberUid',
'posixgroup' => 'posixGroup',
'uniquemember' => 'uniqueMember',
'groupofnames' => 'groupOfNames',
'inetorgperson' => 'inetOrgPerson',
'samaccountname' => 'sAMAccountName'
'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 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]);
}
}