diff --git a/app/Access/Ldap.php b/app/Access/Ldap.php index 12a3d1e71..702d629ce 100644 --- a/app/Access/Ldap.php +++ b/app/Access/Ldap.php @@ -52,13 +52,25 @@ class Ldap * * @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) { 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. * diff --git a/app/Access/LdapService.php b/app/Access/LdapService.php index e822b09a6..7c8d8b18f 100644 --- a/app/Access/LdapService.php +++ b/app/Access/LdapService.php @@ -321,94 +321,105 @@ class LdapService return []; } - $userGroups = $this->groupFilter($user); + $userGroups = $this->extractGroupsFromSearchResponseEntry($user); $allGroups = $this->getGroupsRecursive($userGroups, []); + $formattedGroups = $this->extractGroupNamesFromLdapGroupDns($allGroups); if ($this->config['dump_user_groups']) { throw new JsonDebugException([ - 'details_from_ldap' => $user, - 'parsed_direct_user_groups' => $userGroups, - 'parsed_recursive_user_groups' => $allGroups, + 'details_from_ldap' => $user, + 'parsed_direct_user_groups' => $userGroups, + 'parsed_recursive_user_groups' => $allGroups, + 'parsed_resulting_group_names' => $formattedGroups, ]); } return $allGroups; } - /** - * Get the parent groups of an array of groups. - * - * @throws LdapException - */ - private function getGroupsRecursive(array $groupsArray, array $checked): array + protected function extractGroupNamesFromLdapGroupDns(array $groupDNs): array { - $groupsToAdd = []; - foreach ($groupsArray as $groupName) { - if (in_array($groupName, $checked)) { - continue; + $names = []; + + foreach ($groupDNs as $groupDN) { + $exploded = $this->ldap->explodeDn($groupDN, 1); + if ($exploded !== false && count($exploded) > 0) { + $names[] = $exploded[0]; } - - $parentGroups = $this->getGroupGroups($groupName); - $groupsToAdd = array_merge($groupsToAdd, $parentGroups); - $checked[] = $groupName; } - $groupsArray = array_unique(array_merge($groupsArray, $groupsToAdd), SORT_REGULAR); - - if (empty($groupsToAdd)) { - return $groupsArray; - } - - return $this->getGroupsRecursive($groupsArray, $checked); + return array_unique($names); } /** - * Get the parent groups of a single group. + * Build an array of all relevant groups DNs after recursively scanning + * across parents of the groups given. * * @throws LdapException */ - private function getGroupGroups(string $groupName): array + protected function getGroupsRecursive(array $groupDNs, array $checked): array { + $groupsToAdd = []; + foreach ($groupDNs as $groupDN) { + if (in_array($groupDN, $checked)) { + continue; + } + + $parentGroups = $this->getParentsOfGroup($groupDN); + $groupsToAdd = array_merge($groupsToAdd, $parentGroups); + $checked[] = $groupDN; + } + + $uniqueDNs = array_unique(array_merge($groupDNs, $groupsToAdd), SORT_REGULAR); + + if (empty($groupsToAdd)) { + return $uniqueDNs; + } + + return $this->getGroupsRecursive($uniqueDNs, $checked); + } + + /** + * @throws LdapException + */ + protected function getParentsOfGroup(string $groupDN): array + { + $groupsAttr = strtolower($this->config['group_attribute']); $ldapConnection = $this->getConnection(); $this->bindSystemUser($ldapConnection); $followReferrals = $this->config['follow_referrals'] ? 1 : 0; $this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals); - - $baseDn = $this->config['base_dn']; - $groupsAttr = strtolower($this->config['group_attribute']); - - $groupFilter = 'CN=' . $this->ldap->escape($groupName); - $groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $groupFilter, [$groupsAttr]); - if ($groups['count'] === 0) { + $read = $this->ldap->read($ldapConnection, $groupDN, '(objectClass=*)', [$groupsAttr]); + $results = $this->ldap->getEntries($ldapConnection, $read); + if ($results['count'] === 0) { return []; } - return $this->groupFilter($groups[0]); + return $this->extractGroupsFromSearchResponseEntry($results[0]); } /** - * Filter out LDAP CN and DN language in a ldap search return. - * Gets the base CN (common name) of the string. + * Extract an array of group DN values from the given LDAP search response entry */ - protected function groupFilter(array $userGroupSearchResponse): array + protected function extractGroupsFromSearchResponseEntry(array $ldapEntry): array { $groupsAttr = strtolower($this->config['group_attribute']); - $ldapGroups = []; + $groupDNs = []; $count = 0; - if (isset($userGroupSearchResponse[$groupsAttr]['count'])) { - $count = (int) $userGroupSearchResponse[$groupsAttr]['count']; + if (isset($ldapEntry[$groupsAttr]['count'])) { + $count = (int) $ldapEntry[$groupsAttr]['count']; } for ($i = 0; $i < $count; $i++) { - $dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1); - if (!in_array($dnComponents[0], $ldapGroups)) { - $ldapGroups[] = $dnComponents[0]; + $dn = $ldapEntry[$groupsAttr][$i]; + if (!in_array($dn, $groupDNs)) { + $groupDNs[] = $dn; } } - return $ldapGroups; + return $groupDNs; } /** diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index 3f80f00f4..7c2510eb1 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -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() { $role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);