Merge branch 'feature/allow-to-list-groups-from-a-ldap-backend-9772'
resolves #9772 fixes #9950
This commit is contained in:
commit
f6e67670e6
|
@ -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; ?>
|
||||
|
|
|
@ -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,43 +525,37 @@ 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) {
|
||||
$userQuery = $this->ds
|
||||
->select()
|
||||
->from($this->userClass)
|
||||
->where($this->userNameAttribute, $user->getUsername())
|
||||
->setBase($this->userBaseDn)
|
||||
->setUsePagedResults(false);
|
||||
if ($this->userFilter) {
|
||||
$userQuery->where(new Expression($this->userFilter));
|
||||
}
|
||||
if ($this->isAmbiguous($this->groupClass, $this->groupMemberAttribute)) {
|
||||
$queryValue = $user->getUsername();
|
||||
} elseif (($queryValue = $user->getAdditional('ldap_dn')) === null) {
|
||||
$userQuery = $this->ds
|
||||
->select()
|
||||
->from($this->userClass)
|
||||
->where($this->userNameAttribute, $user->getUsername())
|
||||
->setBase($this->userBaseDn)
|
||||
->setUsePagedResults(false);
|
||||
if ($this->userFilter) {
|
||||
$userQuery->where(new Expression($this->userFilter));
|
||||
}
|
||||
|
||||
if (($userDn = $userQuery->fetchDn()) === null) {
|
||||
return array();
|
||||
}
|
||||
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(),
|
||||
|
@ -661,4 +648,4 @@ class LdapUserGroupBackend /*extends LdapRepository*/ implements UserGroupBacken
|
|||
'group_member_attribute' => 'member'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,13 +727,41 @@ class LdapConnection implements Selectable, Inspectable
|
|||
$count = 0;
|
||||
$entries = array();
|
||||
$entry = ldap_first_entry($ds, $results);
|
||||
$unfoldAttribute = $query->getUnfoldAttribute();
|
||||
do {
|
||||
$count += 1;
|
||||
if (! $serverSorting || $offset === 0 || $offset < $count) {
|
||||
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
|
||||
if ($unfoldAttribute) {
|
||||
$rows = $this->cleanupAttributes(
|
||||
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))
|
||||
&& ($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,12 +871,39 @@ class LdapConnection implements Selectable, Inspectable
|
|||
|
||||
$entry = ldap_first_entry($ds, $results);
|
||||
do {
|
||||
$count += 1;
|
||||
if (! $serverSorting || $offset === 0 || $offset < $count) {
|
||||
$entries[ldap_get_dn($ds, $entry)] = $this->cleanupAttributes(
|
||||
if ($unfoldAttribute) {
|
||||
$rows = $this->cleanupAttributes(
|
||||
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))
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -28,13 +28,27 @@ abstract class LdapRepository extends Repository
|
|||
* @var array
|
||||
*/
|
||||
protected $normedAttributes = array(
|
||||
'uid' => 'uid',
|
||||
'gid' => 'gid',
|
||||
'user' => 'user',
|
||||
'group' => 'group',
|
||||
'member' => 'member',
|
||||
'inetorgperson' => 'inetOrgPerson',
|
||||
'samaccountname' => 'sAMAccountName'
|
||||
'uid' => 'uid',
|
||||
'gid' => 'gid',
|
||||
'user' => 'user',
|
||||
'group' => 'group',
|
||||
'member' => 'member',
|
||||
'memberuid' => 'memberUid',
|
||||
'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 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]);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue