user_ldap: Add support for gidNumber

This patch is based on the work of @dleeuw (https://github.com/dleeuw)
(See https://github.com/nextcloud/server/issues/2640#issuecomment-269615883 for more details).
The difference is user & group data will be written into cache to have
better performance, and functions splited from primaryGroupID series to
make them more readable.

Fixed https://github.com/nextcloud/server/issues/2640

Signed-off-by: Xuanwo <xuanwo@yunify.com>
This commit is contained in:
Xuanwo 2017-03-18 14:56:24 +08:00
parent 9e1e7dac47
commit 8db21ad8c8
6 changed files with 302 additions and 8 deletions

View File

@ -55,6 +55,7 @@ class Configuration {
'ldapIgnoreNamingRules' => null,
'ldapUserDisplayName' => null,
'ldapUserDisplayName2' => null,
'ldapGidNumber' => null,
'ldapUserFilterObjectclass' => null,
'ldapUserFilterGroups' => null,
'ldapUserFilter' => null,
@ -430,6 +431,7 @@ class Configuration {
'ldap_group_filter_mode' => 0,
'ldap_groupfilter_objectclass' => '',
'ldap_groupfilter_groups' => '',
'ldap_gid_number' => 'gidNumber',
'ldap_display_name' => 'displayName',
'ldap_user_display_name_2' => '',
'ldap_group_display_name' => 'cn',
@ -489,6 +491,7 @@ class Configuration {
'ldap_group_filter_mode' => 'ldapGroupFilterMode',
'ldap_groupfilter_objectclass' => 'ldapGroupFilterObjectclass',
'ldap_groupfilter_groups' => 'ldapGroupFilterGroups',
'ldap_gid_number' => 'ldapGidNumber',
'ldap_display_name' => 'ldapUserDisplayName',
'ldap_user_display_name_2' => 'ldapUserDisplayName2',
'ldap_group_display_name' => 'ldapGroupDisplayName',

View File

@ -12,6 +12,7 @@
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roger Szabo <roger.szabo@web.de>
* @author Xuanwo <xuanwo@yunify.com>
*
* @license AGPL-3.0
*
@ -64,6 +65,11 @@ class Connection extends LDAPUtility {
*/
public $hasPrimaryGroups = true;
/**
* @var bool runtime flag that indicates whether supported POSIX gidNumber are available
*/
public $hasGidNumber = true;
//cache handler
protected $cache;

View File

@ -18,6 +18,7 @@
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <pvince81@owncloud.com>
* @author Xuanwo <xuanwo@yunify.com>
*
* @license AGPL-3.0
*
@ -229,9 +230,9 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
}
}
}
$allMembers = array_merge($allMembers, $this->getDynamicGroupMembers($dnGroup));
$this->access->connection->writeToCache($cacheKey, $allMembers);
return $allMembers;
}
@ -263,7 +264,167 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
$allGroups = array_merge($allGroups, $subGroups);
}
}
return $allGroups;
return $allGroups;
}
/**
* translates a gidNumber into an ownCloud internal name
* @param string $gid as given by gidNumber on POSIX LDAP
* @param string $dn a DN that belongs to the same domain as the group
* @return string|bool
*/
public function gidNumber2Name($gid, $dn) {
$cacheKey = 'gidNumberToName' . $gid;
$groupName = $this->access->connection->getFromCache($cacheKey);
if(!is_null($groupName) && isset($groupName)) {
return $groupName;
}
//we need to get the DN from LDAP
$filter = $this->access->combineFilterWithAnd([
$this->access->connection->ldapGroupFilter,
'objectClass=posixGroup',
$this->access->connection->ldapGidNumber . '=' . $gid
]);
$result = $this->access->searchGroups($filter, array('dn'), 1);
if(empty($result)) {
return false;
}
$dn = $result[0]['dn'][0];
//and now the group name
//NOTE once we have separate ownCloud group IDs and group names we can
//directly read the display name attribute instead of the DN
$name = $this->access->dn2groupname($dn);
$this->access->connection->writeToCache($cacheKey, $name);
return $name;
}
/**
* returns the entry's gidNumber
* @param string $dn
* @param string $attribute
* @return string|bool
*/
private function getEntryGidNumber($dn, $attribute) {
$value = $this->access->readAttribute($dn, $attribute);
if(is_array($value) && !empty($value)) {
return $value[0];
}
return false;
}
/**
* returns the group's primary ID
* @param string $dn
* @return string|bool
*/
public function getGroupGidNumber($dn) {
return $this->getEntryGidNumber($dn, 'gidNumber');
}
/**
* returns the user's gidNumber
* @param string $dn
* @return string|bool
*/
public function getUserGidNumber($dn) {
$gidNumber = false;
if($this->access->connection->hasGidNumber) {
$gidNumber = $this->getEntryGidNumber($dn, 'gidNumber');
if($gidNumber === false) {
$this->access->connection->hasGidNumber = false;
}
}
return $gidNumber;
}
/**
* returns a filter for a "users has specific gid" search or count operation
*
* @param string $groupDN
* @param string $search
* @return string
* @throws \Exception
*/
private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
$groupID = $this->getGroupGidNumber($groupDN);
if($groupID === false) {
throw new \Exception('Not a valid group');
}
$filterParts = [];
$filterParts[] = $this->access->getFilterForUserCount();
if ($search !== '') {
$filterParts[] = $this->access->getFilterPartForUserSearch($search);
}
$filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
$filter = $this->access->combineFilterWithAnd($filterParts);
return $filter;
}
/**
* returns a list of users that have the given group as gid number
*
* @param string $groupDN
* @param string $search
* @param int $limit
* @param int $offset
* @return string[]
*/
public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
try {
$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
$users = $this->access->fetchListOfUsers(
$filter,
[$this->access->connection->ldapUserDisplayName, 'dn'],
$limit,
$offset
);
return $this->access->ownCloudUserNames($users);
} catch (\Exception $e) {
return [];
}
}
/**
* returns the number of users that have the given group as gid number
*
* @param string $groupDN
* @param string $search
* @param int $limit
* @param int $offset
* @return int
*/
public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
try {
$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
return (int)$users;
} catch (\Exception $e) {
return 0;
}
}
/**
* gets the gidNumber of a user
* @param string $dn
* @return string
*/
public function getUserGroupByGid($dn) {
$groupID = $this->getUserGidNumber($dn);
if($groupID !== false) {
$groupName = $this->gidNumber2Name($groupID, $dn);
if($groupName !== false) {
return $groupName;
}
}
return false;
}
/**
@ -457,6 +618,7 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
$groups = [];
$primaryGroup = $this->getUserPrimaryGroup($userDN);
$gidGroupName = $this->getUserGroupByGid($userDN);
$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
@ -510,10 +672,13 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
}
}
}
if($primaryGroup !== false) {
$groups[] = $primaryGroup;
}
if($gidGroupName !== false) {
$groups[] = $gidGroupName;
}
$this->access->connection->writeToCache($cacheKey, $groups);
return $groups;
}
@ -547,6 +712,9 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
if($primaryGroup !== false) {
$groups[] = $primaryGroup;
}
if($gidGroupName !== false) {
$groups[] = $gidGroupName;
}
$groups = array_unique($groups, SORT_LOCALE_STRING);
$this->access->connection->writeToCache($cacheKey, $groups);
@ -641,6 +809,14 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
return array();
}
$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
$members = array_keys($this->_groupMembers($groupDN));
if(!$members && empty($posixGroupUsers)) {
//in case users could not be retrieved, return empty result set
$this->access->connection->writeToCache($cacheKey, []);
return [];
}
$groupUsers = array();
$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
$attrs = $this->access->userManager->getAttributes(true);
@ -677,6 +853,10 @@ class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
$groupUsers = array_slice($groupUsers, $offset, $limit);
$groupUsers = array_unique(array_merge($groupUsers, $posixGroupUsers));
natsort($groupUsers);
$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
$groupUsers = array_slice($groupUsers, $offset, $limit);
$this->access->connection->writeToCache($cacheKey, $groupUsers);

View File

@ -15,6 +15,7 @@
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Stefan Weil <sw@weilnetz.de>
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
* @author Xuanwo <xuanwo@yunify.com>
*
* @license AGPL-3.0
*
@ -775,12 +776,12 @@ class Wizard extends LDAPUtility {
/**
* tries to detect the group member association attribute which is
* one of 'uniqueMember', 'memberUid', 'member'
* one of 'uniqueMember', 'memberUid', 'member', 'gidNumber'
* @return string|false, string with the attribute name, false on error
* @throws \Exception
*/
private function detectGroupMemberAssoc() {
$possibleAttrs = array('uniqueMember', 'memberUid', 'member');
$possibleAttrs = array('uniqueMember', 'memberUid', 'member', 'gidNumber');
$filter = $this->configuration->ldapGroupFilter;
if(empty($filter)) {
return false;

View File

@ -97,8 +97,7 @@ style('user_ldap', 'settings');
<p><label for="ldap_group_display_name"><?php p($l->t('Group Display Name Field'));?></label><input type="text" id="ldap_group_display_name" name="ldap_group_display_name" data-default="<?php p($_['ldap_group_display_name_default']); ?>" title="<?php p($l->t('The LDAP attribute to use to generate the groups\'s display name.'));?>" /></p>
<p><label for="ldap_base_groups"><?php p($l->t('Base Group Tree'));?></label><textarea id="ldap_base_groups" name="ldap_base_groups" placeholder="<?php p($l->t('One Group Base DN per line'));?>" data-default="<?php p($_['ldap_base_groups_default']); ?>" title="<?php p($l->t('Base Group Tree'));?>"></textarea></p>
<p><label for="ldap_attributes_for_group_search"><?php p($l->t('Group Search Attributes'));?></label><textarea id="ldap_attributes_for_group_search" name="ldap_attributes_for_group_search" placeholder="<?php p($l->t('Optional; one attribute per line'));?>" data-default="<?php p($_['ldap_attributes_for_group_search_default']); ?>" title="<?php p($l->t('Group Search Attributes'));?>"></textarea></p>
<p><label for="ldap_group_member_assoc_attribute"><?php p($l->t('Group-Member association'));?></label><select id="ldap_group_member_assoc_attribute" name="ldap_group_member_assoc_attribute" data-default="<?php p($_['ldap_group_member_assoc_attribute_default']); ?>" ><option value="uniqueMember"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'uniqueMember')) p(' selected'); ?>>uniqueMember</option><option value="memberUid"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'memberUid')) p(' selected'); ?>>memberUid</option><option value="member"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'member')) p(' selected'); ?>>member (AD)</option></select></p>
<p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL'));?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)'));?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p>
<p><label for="ldap_group_member_assoc_attribute"><?php p($l->t('Group-Member association'));?></label><select id="ldap_group_member_assoc_attribute" name="ldap_group_member_assoc_attribute" data-default="<?php p($_['ldap_group_member_assoc_attribute_default']); ?>" ><option value="uniqueMember"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'uniqueMember')) p(' selected'); ?>>uniqueMember</option><option value="memberUid"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'memberUid')) p(' selected'); ?>>memberUid</option><option value="member"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'member')) p(' selected'); ?>>member (AD)</option><option value="gidNumber"<?php if (isset($_['ldap_group_member_assoc_attribute']) && ($_['ldap_group_member_assoc_attribute'] === 'gidNumber')) p(' selected'); ?>>gidNumber</option></select></p> <p><label for="ldap_dynamic_group_member_url"><?php p($l->t('Dynamic Group Member URL'));?></label><input type="text" id="ldap_dynamic_group_member_url" name="ldap_dynamic_group_member_url" title="<?php p($l->t('The LDAP attribute that on group objects contains an LDAP search URL that determines what objects belong to the group. (An empty setting disables dynamic group membership functionality.)'));?>" data-default="<?php p($_['ldap_dynamic_group_member_url_default']); ?>" /></p>
<p><label for="ldap_nested_groups"><?php p($l->t('Nested Groups'));?></label><input type="checkbox" id="ldap_nested_groups" name="ldap_nested_groups" value="1" data-default="<?php p($_['ldap_nested_groups_default']); ?>" title="<?php p($l->t('When switched on, groups that contain groups are supported. (Only works if the group member attribute contains DNs.)'));?>" /></p>
<p><label for="ldap_paging_size"><?php p($l->t('Paging chunksize'));?></label><input type="number" id="ldap_paging_size" name="ldap_paging_size" title="<?php p($l->t('Chunksize used for paged LDAP searches that may return bulky results like user or group enumeration. (Setting it 0 disables paged LDAP searches in those situations.)'));?>" data-default="<?php p($_['ldap_paging_size_default']); ?>" /></p>
<p><label for="ldap_turn_on_pwd_change"><?php p($l->t('Enable LDAP password changes per user'));?></label><span class="inlinetable"><span class="tablerow left"><input type="checkbox" id="ldap_turn_on_pwd_change" name="ldap_turn_on_pwd_change" value="1" data-default="<?php p($_['ldap_turn_on_pwd_change_default']); ?>" title="<?php p($l->t('Allow LDAP users to change their password and allow Super Administrators and Group Administrators to change the password of their LDAP users. Only works when access control policies are configured accordingly on the LDAP server. As passwords are sent in plaintext to the LDAP server, transport encryption must be used and password hashing should be configured on the LDAP server.'));?>" /><span class="tablecell"><?php p($l->t('(New password is sent as plain text to LDAP)'));?></span></span>

View File

@ -9,6 +9,7 @@
* @author Morris Jobke <hey@morrisjobke.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <pvince81@owncloud.com>
* @author Xuanwo <xuanwo@yunify.com>
*
* @license AGPL-3.0
*
@ -142,6 +143,107 @@ class Group_LDAPTest extends \Test\TestCase {
$this->assertSame(2, $users);
}
public function testGidNumber2NameSuccess() {
$access = $this->getAccessMock();
$this->enableGroups($access);
$userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar';
$access->expects($this->once())
->method('searchGroups')
->will($this->returnValue([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]));
$access->expects($this->once())
->method('dn2groupname')
->with('cn=foo,dc=barfoo,dc=bar')
->will($this->returnValue('MyGroup'));
$groupBackend = new GroupLDAP($access);
$group = $groupBackend->gidNumber2Name('3117', $userDN);
$this->assertSame('MyGroup', $group);
}
public function testGidNumberID2NameNoGroup() {
$access = $this->getAccessMock();
$this->enableGroups($access);
$userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar';
$access->expects($this->once())
->method('searchGroups')
->will($this->returnValue(array()));
$access->expects($this->never())
->method('dn2groupname');
$groupBackend = new GroupLDAP($access);
$group = $groupBackend->gidNumber2Name('3117', $userDN);
$this->assertSame(false, $group);
}
public function testGidNumberID2NameNoName() {
$access = $this->getAccessMock();
$this->enableGroups($access);
$userDN = 'cn=alice,cn=foo,dc=barfoo,dc=bar';
$access->expects($this->once())
->method('searchGroups')
->will($this->returnValue([['dn' => ['cn=foo,dc=barfoo,dc=bar']]]));
$access->expects($this->once())
->method('dn2groupname')
->will($this->returnValue(false));
$groupBackend = new GroupLDAP($access);
$group = $groupBackend->gidNumber2Name('3117', $userDN);
$this->assertSame(false, $group);
}
public function testGetEntryGidNumberValue() {
$access = $this->getAccessMock();
$this->enableGroups($access);
$dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar';
$attr = 'gidNumber';
$access->expects($this->once())
->method('readAttribute')
->with($dn, $attr)
->will($this->returnValue(array('3117')));
$groupBackend = new GroupLDAP($access);
$gid = $groupBackend->getGroupGidNumber($dn);
$this->assertSame('3117', $gid);
}
public function testGetEntryGidNumberNoValue() {
$access = $this->getAccessMock();
$this->enableGroups($access);
$dn = 'cn=foobar,cn=foo,dc=barfoo,dc=bar';
$attr = 'gidNumber';
$access->expects($this->once())
->method('readAttribute')
->with($dn, $attr)
->will($this->returnValue(false));
$groupBackend = new GroupLDAP($access);
$gid = $groupBackend->getGroupGidNumber($dn);
$this->assertSame(false, $gid);
}
public function testPrimaryGroupID2NameSuccess() {
$access = $this->getAccessMock();
$this->enableGroups($access);
@ -401,6 +503,7 @@ class Group_LDAPTest extends \Test\TestCase {
$dn = 'cn=userX,dc=foobar';
$access->connection->hasPrimaryGroups = false;
$access->connection->hasGidNumber = false;
$access->expects($this->any())
->method('username2dn')
@ -441,6 +544,7 @@ class Group_LDAPTest extends \Test\TestCase {
$dn = 'cn=userX,dc=foobar';
$access->connection->hasPrimaryGroups = false;
$access->connection->hasGidNumber = false;
$access->expects($this->once())
->method('username2dn')
@ -477,6 +581,7 @@ class Group_LDAPTest extends \Test\TestCase {
$dn = 'cn=userX,dc=foobar';
$access->connection->hasPrimaryGroups = false;
$access->connection->hasGidNumber = false;
$access->expects($this->exactly(2))
->method('username2dn')