LDAP: Updated recursive group search to query by DN

Added test to cover, added pre-change.
Need to test post-changes and fix tests.
This commit is contained in:
Dan Brown 2024-08-28 15:39:05 +01:00
parent 8cef998f49
commit 1b4ed69f41
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 107 additions and 46 deletions

View File

@ -52,13 +52,25 @@ class Ldap
* *
* @param resource|\LDAP\Connection $ldapConnection * @param resource|\LDAP\Connection $ldapConnection
* *
* @return resource|\LDAP\Result * @return \LDAP\Result|array|false
*/ */
public function search($ldapConnection, string $baseDn, string $filter, array $attributes = null) public function search($ldapConnection, string $baseDn, string $filter, array $attributes = null)
{ {
return ldap_search($ldapConnection, $baseDn, $filter, $attributes); return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
} }
/**
* Read an entry from the LDAP tree.
*
* @param resource|\Ldap\Connection $ldapConnection
*
* @return \LDAP\Result|array|false
*/
public function read($ldapConnection, string $baseDn, string $filter, array $attributes = null)
{
return ldap_read($ldapConnection, $baseDn, $filter, $attributes);
}
/** /**
* Get entries from an LDAP search result. * Get entries from an LDAP search result.
* *

View File

@ -321,94 +321,105 @@ class LdapService
return []; return [];
} }
$userGroups = $this->groupFilter($user); $userGroups = $this->extractGroupsFromSearchResponseEntry($user);
$allGroups = $this->getGroupsRecursive($userGroups, []); $allGroups = $this->getGroupsRecursive($userGroups, []);
$formattedGroups = $this->extractGroupNamesFromLdapGroupDns($allGroups);
if ($this->config['dump_user_groups']) { if ($this->config['dump_user_groups']) {
throw new JsonDebugException([ throw new JsonDebugException([
'details_from_ldap' => $user, 'details_from_ldap' => $user,
'parsed_direct_user_groups' => $userGroups, 'parsed_direct_user_groups' => $userGroups,
'parsed_recursive_user_groups' => $allGroups, 'parsed_recursive_user_groups' => $allGroups,
'parsed_resulting_group_names' => $formattedGroups,
]); ]);
} }
return $allGroups; return $allGroups;
} }
protected function extractGroupNamesFromLdapGroupDns(array $groupDNs): array
{
$names = [];
foreach ($groupDNs as $groupDN) {
$exploded = $this->ldap->explodeDn($groupDN, 1);
if ($exploded !== false && count($exploded) > 0) {
$names[] = $exploded[0];
}
}
return array_unique($names);
}
/** /**
* Get the parent groups of an array of groups. * Build an array of all relevant groups DNs after recursively scanning
* across parents of the groups given.
* *
* @throws LdapException * @throws LdapException
*/ */
private function getGroupsRecursive(array $groupsArray, array $checked): array protected function getGroupsRecursive(array $groupDNs, array $checked): array
{ {
$groupsToAdd = []; $groupsToAdd = [];
foreach ($groupsArray as $groupName) { foreach ($groupDNs as $groupDN) {
if (in_array($groupName, $checked)) { if (in_array($groupDN, $checked)) {
continue; continue;
} }
$parentGroups = $this->getGroupGroups($groupName); $parentGroups = $this->getParentsOfGroup($groupDN);
$groupsToAdd = array_merge($groupsToAdd, $parentGroups); $groupsToAdd = array_merge($groupsToAdd, $parentGroups);
$checked[] = $groupName; $checked[] = $groupDN;
} }
$groupsArray = array_unique(array_merge($groupsArray, $groupsToAdd), SORT_REGULAR); $uniqueDNs = array_unique(array_merge($groupDNs, $groupsToAdd), SORT_REGULAR);
if (empty($groupsToAdd)) { if (empty($groupsToAdd)) {
return $groupsArray; return $uniqueDNs;
} }
return $this->getGroupsRecursive($groupsArray, $checked); return $this->getGroupsRecursive($uniqueDNs, $checked);
} }
/** /**
* Get the parent groups of a single group.
*
* @throws LdapException * @throws LdapException
*/ */
private function getGroupGroups(string $groupName): array protected function getParentsOfGroup(string $groupDN): array
{ {
$groupsAttr = strtolower($this->config['group_attribute']);
$ldapConnection = $this->getConnection(); $ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection); $this->bindSystemUser($ldapConnection);
$followReferrals = $this->config['follow_referrals'] ? 1 : 0; $followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$read = $this->ldap->read($ldapConnection, $groupDN, '(objectClass=*)', [$groupsAttr]);
$baseDn = $this->config['base_dn']; $results = $this->ldap->getEntries($ldapConnection, $read);
$groupsAttr = strtolower($this->config['group_attribute']); if ($results['count'] === 0) {
$groupFilter = 'CN=' . $this->ldap->escape($groupName);
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $groupFilter, [$groupsAttr]);
if ($groups['count'] === 0) {
return []; return [];
} }
return $this->groupFilter($groups[0]); return $this->extractGroupsFromSearchResponseEntry($results[0]);
} }
/** /**
* Filter out LDAP CN and DN language in a ldap search return. * Extract an array of group DN values from the given LDAP search response entry
* Gets the base CN (common name) of the string.
*/ */
protected function groupFilter(array $userGroupSearchResponse): array protected function extractGroupsFromSearchResponseEntry(array $ldapEntry): array
{ {
$groupsAttr = strtolower($this->config['group_attribute']); $groupsAttr = strtolower($this->config['group_attribute']);
$ldapGroups = []; $groupDNs = [];
$count = 0; $count = 0;
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) { if (isset($ldapEntry[$groupsAttr]['count'])) {
$count = (int) $userGroupSearchResponse[$groupsAttr]['count']; $count = (int) $ldapEntry[$groupsAttr]['count'];
} }
for ($i = 0; $i < $count; $i++) { for ($i = 0; $i < $count; $i++) {
$dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1); $dn = $ldapEntry[$groupsAttr][$i];
if (!in_array($dnComponents[0], $ldapGroups)) { if (!in_array($dn, $groupDNs)) {
$ldapGroups[] = $dnComponents[0]; $groupDNs[] = $dn;
} }
} }
return $ldapGroups; return $groupDNs;
} }
/** /**

View File

@ -435,6 +435,44 @@ class LdapTest extends TestCase
]); ]);
} }
public function test_recursive_group_search_queries_via_full_dn()
{
app('config')->set([
'services.ldap.user_to_groups' => true,
'services.ldap.group_attribute' => 'memberOf',
]);
$userResp = ['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => 'dc=test,' . config('services.ldap.base_dn'),
'mail' => [$this->mockUser->email],
]];
$groupResp = ['count' => 1,
0 => [
'dn' => 'dc=test,' . config('services.ldap.base_dn'),
'memberof' => [
'count' => 1,
0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
],
],
];
$this->commonLdapMocks(1, 1, 3, 4, 3, 1);
$escapedName = ldap_escape($this->mockUser->name);
$this->mockLdap->shouldReceive('searchAndGetEntries')->twice()
->with($this->resourceId, config('services.ldap.base_dn'), "(&(uid={$escapedName}))", \Mockery::type('array'))
->andReturn($userResp, $groupResp);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
->with($this->resourceId, config('services.ldap.base_dn'), $groupResp[0]['dn'], ['memberof'])
->andReturn(['count' => 0]);
$resp = $this->mockUserLogin();
$resp->assertRedirect('/');
}
public function test_external_auth_id_visible_in_roles_page_when_ldap_active() public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
{ {
$role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']); $role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);