diff --git a/app/modules/web/Controllers/ConfigLdap/CheckController.php b/app/modules/web/Controllers/ConfigLdap/CheckController.php index 9aa5797e..df631d4d 100644 --- a/app/modules/web/Controllers/ConfigLdap/CheckController.php +++ b/app/modules/web/Controllers/ConfigLdap/CheckController.php @@ -89,10 +89,10 @@ final class CheckController extends SimpleControllerBase $this->template->assign('header', __('Results')); return $this->returnJsonResponseData( - ['template' => $this->template->render(), 'items' => $data['results']], + ['template' => $this->template->render(), 'items' => $data->getResults()], JsonMessage::JSON_SUCCESS, __u('LDAP connection OK'), - [sprintf(__('Objects found: %d'), $data['count'])] + [sprintf(__('Objects found: %d'), $data->count())] ); } catch (Exception $e) { processException($e); diff --git a/app/modules/web/Controllers/ConfigLdap/CheckImportController.php b/app/modules/web/Controllers/ConfigLdap/CheckImportController.php index acbb765d..5826c53e 100644 --- a/app/modules/web/Controllers/ConfigLdap/CheckImportController.php +++ b/app/modules/web/Controllers/ConfigLdap/CheckImportController.php @@ -93,13 +93,13 @@ final class CheckImportController extends SimpleControllerBase $this->template->addTemplate('results', 'itemshow'); $this->template->assign('header', __('Results')); - $this->template->assign('results', $data); + $this->template->assign('results', $data->getResults()); return $this->returnJsonResponseData( - ['template' => $this->template->render(), 'items' => $data['results']], + ['template' => $this->template->render(), 'items' => $data->getResults()], JsonMessage::JSON_SUCCESS, __u('LDAP connection OK'), - [sprintf(__('Objects found: %d'), $data['count'])] + [sprintf(__('Objects found: %d'), $data->count())] ); } catch (Exception $e) { processException($e); diff --git a/lib/SP/Domain/Auth/Dtos/LdapCheckResults.php b/lib/SP/Domain/Auth/Dtos/LdapCheckResults.php new file mode 100644 index 00000000..8cba0019 --- /dev/null +++ b/lib/SP/Domain/Auth/Dtos/LdapCheckResults.php @@ -0,0 +1,55 @@ +. + */ + +namespace SP\Domain\Auth\Dtos; + +/** + * Class LdapCheckResults + */ +final class LdapCheckResults +{ + + private array $results = []; + + public function __construct(array $items, ?string $type = null) + { + $this->addItems($items, $type); + } + + + public function addItems(array $items, ?string $type = null): void + { + $this->results[] = ['items' => $items, 'type' => $type]; + } + + public function count(): int + { + return (int)array_sum(array_map(fn(array $result) => count($result['items']), $this->results)); + } + + public function getResults(): array + { + return $this->results; + } +} diff --git a/lib/SP/Domain/Auth/Ports/LdapCheckService.php b/lib/SP/Domain/Auth/Ports/LdapCheckService.php index ef1303b2..98296ed0 100644 --- a/lib/SP/Domain/Auth/Ports/LdapCheckService.php +++ b/lib/SP/Domain/Auth/Ports/LdapCheckService.php @@ -25,6 +25,7 @@ namespace SP\Domain\Auth\Ports; +use SP\Domain\Auth\Dtos\LdapCheckResults; use SP\Providers\Auth\Ldap\LdapException; /** @@ -37,10 +38,10 @@ interface LdapCheckService /** * @throws LdapException */ - public function getObjects(bool $includeGroups = true): array; + public function getObjects(bool $includeGroups = true): LdapCheckResults; /** * @throws LdapException */ - public function getObjectsByFilter(string $filter): array; + public function getObjectsByFilter(string $filter): LdapCheckResults; } diff --git a/lib/SP/Domain/Auth/Services/LdapCheck.php b/lib/SP/Domain/Auth/Services/LdapCheck.php index 123f1da8..706a0df8 100644 --- a/lib/SP/Domain/Auth/Services/LdapCheck.php +++ b/lib/SP/Domain/Auth/Services/LdapCheck.php @@ -25,6 +25,7 @@ namespace SP\Domain\Auth\Services; use SP\Core\Application; +use SP\Domain\Auth\Dtos\LdapCheckResults; use SP\Domain\Auth\Ports\LdapActionsService; use SP\Domain\Auth\Ports\LdapCheckService; use SP\Domain\Auth\Ports\LdapConnectionInterface; @@ -50,23 +51,87 @@ final class LdapCheck extends Service implements LdapCheckService /** * @throws LdapException */ - public function getObjectsByFilter(string $filter, ?LdapParams $ldapParams = null): array + public function getObjectsByFilter(string $filter, ?LdapParams $ldapParams = null): LdapCheckResults + { + return new LdapCheckResults( + self::getObjectsWithAttributes($this->getLdap($ldapParams)->actions(), $filter, ['dn']) + ); + } + + /** + * @throws LdapException + */ + private static function getObjectsWithAttributes( + LdapActionsService $ldapActionsService, + string $filter, + array $attributes + ): array { + return self::ldapResultsMapper( + iterator_to_array($ldapActionsService->getObjects($filter, $attributes)->getIterator()), + $attributes + ); + } + + /** + * Obtener los datos de una búsqueda de LDAP de un atributo + * + * @param array $data + * @param string[] $attributes + * + * @return array + */ + private static function ldapResultsMapper(array $data, array $attributes = ['dn']): array + { + $attributesKey = array_flip($attributes); + + return array_map( + static function (mixed $value) { + if (is_array($value)) { + foreach ($value as $k => $v) { + if ($k !== 'count') { + return $v; + } + } + } + + return $value; + }, + array_filter($data, static fn(mixed $d) => is_array($d) && array_intersect_key($d, $attributesKey)) + ); + } + + /** + * @throws LdapException + */ + public function getObjects(bool $includeGroups = true, ?LdapParams $ldapParams = null): LdapCheckResults { $ldap = $this->getLdap($ldapParams); - $objects = $this->ldapResultsMapper( - $ldap->actions()->getObjects($filter, ['dn']) + $ldapActionsService = $ldap->actions(); + + $indirectFilterItems = + self::getObjectsWithAttributes($ldapActionsService, $ldap->getGroupMembershipIndirectFilter(), ['dn']); + + $directFilterItems = self::getObjectsWithAttributes( + $ldapActionsService, + $ldap->getGroupMembershipDirectFilter(), + ['member', 'memberUid', 'uniqueMember'] ); - return [ - 'count' => count($objects), - 'results' => [ - [ - 'icon' => '', - 'items' => $objects, - ], - ], - ]; + + $ldapCheckResults = new LdapCheckResults( + array_values(array_unique(array_merge($indirectFilterItems, $directFilterItems))), + 'person' + ); + + if ($includeGroups) { + $ldapCheckResults->addItems( + self::getObjectsWithAttributes($ldapActionsService, $ldap->getGroupObjectFilter(), ['dn']), + 'group' + ); + } + + return $ldapCheckResults; } /** @@ -84,88 +149,4 @@ final class LdapCheck extends Service implements LdapCheckService $ldapParams ); } - - /** - * Obtener los datos de una búsqueda de LDAP de un atributo - * - * @param array $data - * @param string[] $attributes - * - * @return array - */ - private function ldapResultsMapper( - array $data, - array $attributes = ['dn'] - ): array { - $out = []; - - foreach ($data as $result) { - if (is_array($result)) { - foreach ($result as $ldapAttribute => $value) { - if (in_array(strtolower($ldapAttribute), $attributes, true)) { - if (is_array($value)) { - unset($value['count']); - - $out = array_merge($out, $value); - } else { - $out[] = $value; - } - } - } - } - } - - return $out; - } - - /** - * @throws LdapException - */ - public function getObjects(bool $includeGroups = true, ?LdapParams $ldapParams = null): array - { - $ldap = $this->getLdap($ldapParams); - - $ldapActions = $ldap->actions(); - - $data = ['count' => 0, 'results' => []]; - - $indirectFilterItems = $this->ldapResultsMapper( - $ldapActions->getObjects($ldap->getGroupMembershipIndirectFilter(), ['dn']) - ); - - $directFilterItems = $this->ldapResultsMapper( - $ldapActions->getObjects( - $ldap->getGroupMembershipDirectFilter(), - ['member', 'memberUid', 'uniqueMember'] - ), - ['member', 'memberUid', 'uniqueMember'] - ); - - $userItems = array_unique(array_merge($indirectFilterItems, $directFilterItems)); - - $data['results'][] = [ - 'icon' => 'person', - 'items' => array_values($userItems), - ]; - - if ($includeGroups) { - $groupItems = $this->ldapResultsMapper( - $ldapActions->getObjects($ldap->getGroupObjectFilter(), ['dn']) - ); - - $data['results'][] = [ - 'icon' => 'group', - 'items' => $groupItems, - ]; - } - - array_walk( - $data['results'], - static function ($value) use (&$data) { - $data['count'] += count($value['items']); - } - ); - - return $data; - } } diff --git a/lib/SP/Providers/Auth/Ldap/LdapResults.php b/lib/SP/Providers/Auth/Ldap/LdapResults.php index 84b377b9..6fa37656 100644 --- a/lib/SP/Providers/Auth/Ldap/LdapResults.php +++ b/lib/SP/Providers/Auth/Ldap/LdapResults.php @@ -29,9 +29,9 @@ use Iterator; /** * Class LdapResults */ -class LdapResults +readonly class LdapResults { - public function __construct(private readonly int $count, private readonly Iterator $iterator) + public function __construct(private int $count, private Iterator $iterator) { } diff --git a/tests/SPT/Domain/Auth/Services/LdapCheckTest.php b/tests/SPT/Domain/Auth/Services/LdapCheckTest.php new file mode 100644 index 00000000..2895bd0b --- /dev/null +++ b/tests/SPT/Domain/Auth/Services/LdapCheckTest.php @@ -0,0 +1,268 @@ +. + */ + +namespace SPT\Domain\Auth\Services; + +use ArrayIterator; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\MockObject\MockObject; +use SP\Domain\Auth\Ports\LdapActionsService; +use SP\Domain\Auth\Ports\LdapConnectionInterface; +use SP\Domain\Auth\Services\LdapCheck; +use SP\Providers\Auth\Ldap\LdapException; +use SP\Providers\Auth\Ldap\LdapParams; +use SP\Providers\Auth\Ldap\LdapResults; +use SP\Providers\Auth\Ldap\LdapTypeEnum; +use SPT\UnitaryTestCase; + +/** + * Class LdapCheckTest + */ +#[Group('unitary')] +class LdapCheckTest extends UnitaryTestCase +{ + + private LdapCheck $ldapCheck; + private LdapConnectionInterface|MockObject $ldapConnection; + private MockObject|LdapActionsService $ldapActionsService; + + /** + * @throws LdapException + */ + public function testGetObjectsByFilterWithParams() + { + $ldapParams = new LdapParams('a_server', LdapTypeEnum::STD, 'a_dn', 'a_pass'); + + $ldapData = $this->getLdapData(); + + $ldapResults = new LdapResults(10, new ArrayIterator($ldapData)); + + $this->ldapActionsService + ->expects($this->once()) + ->method('getObjects') + ->with('a_filter', ['dn']) + ->willReturn($ldapResults); + + $out = $this->ldapCheck->getObjectsByFilter('a_filter', $ldapParams); + + $this->assertEquals(5, $out->count()); + + $results = $out->getResults(); + + foreach ($ldapData as $index => $data) { + $this->assertEquals($data['dn'], $results[0]['items'][$index]); + } + } + + /** + * @return array|array[] + */ + private function getLdapData(): array + { + return array_map( + static fn() => [ + 'count' => self::$faker->randomNumber(2), + 'dn' => self::$faker->userName(), + 'email' => [self::$faker->email(), self::$faker->email()], + 'member' => self::$faker->userName(), + 'memberUid' => self::$faker->uuid(), + 'uniqueMember' => self::$faker->uuid() + ], + range(0, 4) + ); + } + + /** + * @throws LdapException + */ + public function testGetObjectsByFilterWithConnectionException() + { + $ldapParams = new LdapParams('a_server', LdapTypeEnum::STD, 'a_dn', 'a_pass'); + + $this->ldapActionsService + ->expects($this->never()) + ->method('getObjects'); + + $this->ldapConnection + ->expects($this->once()) + ->method('checkConnection') + ->willThrowException(LdapException::error('test')); + + $this->expectException(LdapException::class); + $this->expectExceptionMessage('test'); + + $this->ldapCheck->getObjectsByFilter('a_filter', $ldapParams); + } + + /** + * @throws LdapException + */ + public function testGetObjectsByFilterWithObjectsException() + { + $ldapParams = new LdapParams('a_server', LdapTypeEnum::STD, 'a_dn', 'a_pass'); + + $this->ldapActionsService + ->expects($this->once()) + ->method('getObjects') + ->willThrowException(LdapException::error('test')); + + $this->expectException(LdapException::class); + $this->expectExceptionMessage('test'); + + $this->ldapCheck->getObjectsByFilter('a_filter', $ldapParams); + } + + /** + * @throws LdapException + */ + public function testGetObjectsWithParams() + { + $ldapParams = new LdapParams('a_server', LdapTypeEnum::STD, 'a_dn', 'a_pass'); + $ldapParams->setFilterUserObject('a_user_filter'); + $ldapParams->setFilterGroupObject('a_group_filter'); + + $ldapData = $this->getLdapData(); + + $ldapResults = new LdapResults(10, new ArrayIterator($ldapData)); + + $this->ldapActionsService + ->expects($this->exactly(3)) + ->method('getObjects') + ->with( + ... + self::withConsecutive( + ['a_user_filter', ['dn']], + ['a_user_filter', ['member', 'memberUid', 'uniqueMember']], + ['a_group_filter', ['dn']], + ) + ) + ->willReturn($ldapResults); + + $out = $this->ldapCheck->getObjects(true, $ldapParams); + + $this->assertEquals(10, $out->count()); + + $results = $out->getResults(); + + foreach ($ldapData as $index => $data) { + $this->assertEquals($data['dn'], $results[0]['items'][$index]); + } + } + + /** + * @throws LdapException + */ + public function testGetObjectsWithNoParams() + { + $ldapParams = new LdapParams('a_server', LdapTypeEnum::STD, 'a_dn', 'a_pass'); + + $ldapDataUsers = $this->getLdapData(); + $ldapDataGroups = $this->getLdapData(); + + $ldapResultsUsers = new LdapResults(10, new ArrayIterator($ldapDataUsers)); + $ldapResultsGroups = new LdapResults(10, new ArrayIterator($ldapDataGroups)); + + $this->ldapActionsService + ->expects($this->exactly(3)) + ->method('getObjects') + ->with( + ... + self::withConsecutive( + ['(|(objectClass=inetOrgPerson)(objectClass=person)(objectClass=simpleSecurityObject))', ['dn']], + [ + '(|(objectClass=inetOrgPerson)(objectClass=person)(objectClass=simpleSecurityObject))', + ['member', 'memberUid', 'uniqueMember'] + ], + ['(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)(objectClass=group))', ['dn']], + ) + ) + ->willReturn($ldapResultsUsers, $ldapResultsUsers, $ldapResultsGroups); + + $out = $this->ldapCheck->getObjects(true, $ldapParams); + + $this->assertEquals(10, $out->count()); + + $results = $out->getResults(); + + foreach ($ldapDataUsers as $index => $data) { + $this->assertEquals($data['dn'], $results[0]['items'][$index]); + } + + foreach ($ldapResultsGroups as $index => $data) { + $this->assertEquals($data['dn'], $results[1]['items'][$index]); + } + } + + /** + * @throws LdapException + */ + public function testGetObjectsWitConnectionException() + { + $ldapParams = new LdapParams('a_server', LdapTypeEnum::STD, 'a_dn', 'a_pass'); + + $this->ldapActionsService + ->expects($this->never()) + ->method('getObjects'); + + $this->ldapConnection + ->expects($this->once()) + ->method('checkConnection') + ->willThrowException(LdapException::error('test')); + + $this->expectException(LdapException::class); + $this->expectExceptionMessage('test'); + + $this->ldapCheck->getObjects(true, $ldapParams); + } + + /** + * @throws LdapException + */ + public function testGetObjectsWitObjectsException() + { + $ldapParams = new LdapParams('a_server', LdapTypeEnum::STD, 'a_dn', 'a_pass'); + + $this->ldapActionsService + ->expects($this->once()) + ->method('getObjects') + ->willThrowException(LdapException::error('test')); + + $this->expectException(LdapException::class); + $this->expectExceptionMessage('test'); + + $this->ldapCheck->getObjects(true, $ldapParams); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->ldapConnection = $this->createMock(LdapConnectionInterface::class); + $this->ldapConnection->method('mutate')->willReturnSelf(); + $this->ldapActionsService = $this->createMock(LdapActionsService::class); + $this->ldapActionsService->method('mutate')->willReturnSelf(); + + $this->ldapCheck = new LdapCheck($this->application, $this->ldapConnection, $this->ldapActionsService); + } +}