Skip to content

Commit be12203

Browse files
committed
Intitial implementation
1 parent ebb6914 commit be12203

File tree

5 files changed

+235
-24
lines changed

5 files changed

+235
-24
lines changed

src/Controllers/Federation/EntityStatementController.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace SimpleSAML\Module\oidc\Controllers\Federation;
66

7-
use SimpleSAML\Module\oidc\Codebooks\RoutesEnum;
87
use SimpleSAML\Module\oidc\Helpers;
98
use SimpleSAML\Module\oidc\ModuleConfig;
109
use SimpleSAML\Module\oidc\Repositories\ClientRepository;
@@ -95,11 +94,10 @@ public function configuration(): Response
9594
ClaimsEnum::HomepageUri->value => $this->moduleConfig->getHomepageUri(),
9695
],
9796
)),
98-
ClaimsEnum::FederationFetchEndpoint->value =>
99-
$this->moduleConfig->getModuleUrl(RoutesEnum::FederationFetch->value),
97+
ClaimsEnum::FederationFetchEndpoint->value => $this->routes->urlFederationFetch(),
98+
ClaimsEnum::FederationListEndpoint->value => $this->routes->urlFederationList(),
10099
// TODO v7 mivanci Add when ready. Use ClaimsEnum for keys.
101100
// https://openid.net/specs/openid-federation-1_0.html#name-federation-entity
102-
//'federation_list_endpoint',
103101
//'federation_resolve_endpoint',
104102
//'federation_trust_mark_status_endpoint',
105103
//'federation_trust_mark_list_endpoint',
@@ -233,7 +231,7 @@ public function fetch(Request $request): Response
233231
return $this->prepareEntityStatementResponse((string)$cachedSubordinateStatement);
234232
}
235233

236-
$client = $this->clientRepository->findByEntityIdentifier($subject);
234+
$client = $this->clientRepository->findFederatedByEntityIdentifier($subject);
237235
if (empty($client)) {
238236
return $this->routes->newJsonErrorResponse(
239237
ErrorsEnum::NotFound->value,

src/Controllers/Federation/SubordinateListingsController.php

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44

55
namespace SimpleSAML\Module\oidc\Controllers\Federation;
66

7-
use SimpleSAML\Module\oidc\Helpers;
7+
use SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface;
88
use SimpleSAML\Module\oidc\ModuleConfig;
99
use SimpleSAML\Module\oidc\Repositories\ClientRepository;
1010
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
11-
use SimpleSAML\Module\oidc\Services\LoggerService;
1211
use SimpleSAML\Module\oidc\Utils\Routes;
1312
use SimpleSAML\OpenID\Codebooks\ErrorsEnum;
1413
use SimpleSAML\OpenID\Codebooks\ParamsEnum;
@@ -21,11 +20,9 @@ class SubordinateListingsController
2120
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
2221
*/
2322
public function __construct(
24-
private readonly ModuleConfig $moduleConfig,
25-
private readonly ClientRepository $clientRepository,
26-
private readonly Helpers $helpers,
27-
private readonly Routes $routes,
28-
private readonly LoggerService $loggerService,
23+
protected readonly ModuleConfig $moduleConfig,
24+
protected readonly ClientRepository $clientRepository,
25+
protected readonly Routes $routes,
2926
) {
3027
if (!$this->moduleConfig->getFederationEnabled()) {
3128
throw OidcServerException::forbidden('federation capabilities not enabled');
@@ -52,15 +49,19 @@ public function list(Request $request): Response
5249
return $this->routes->newJsonErrorResponse(
5350
ErrorsEnum::UnsupportedParameter->value,
5451
'Unsupported parameter: ' . implode(', ', $intersectedParams),
52+
400,
5553
);
5654
}
5755

58-
dd($request->query->all());
56+
$subordinateEntityIdList = array_filter(array_map(
57+
function (ClientEntityInterface $clientEntity): ?string {
58+
return $clientEntity->getEntityIdentifier();
59+
},
60+
$this->clientRepository->findAllFederated(),
61+
));
5962

60-
61-
if ($entityTypes = $request->query->all(ParamsEnum::EntityType->value)) {
62-
}
63-
64-
return new Response();
63+
return $this->routes->newJsonResponse(
64+
$subordinateEntityIdList,
65+
);
6566
}
6667
}

src/Repositories/ClientRepository.php

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,10 @@ public function findByEntityIdentifier(string $entityIdentifier, ?string $owner
153153
<<<EOS
154154
SELECT * FROM {$this->getTableName()}
155155
WHERE
156-
entity_identifier = :entity_identifier AND
157-
is_enabled = :is_enabled AND
158-
is_federated = :is_federated
156+
entity_identifier = :entity_identifier
159157
EOS,
160158
[
161159
'entity_identifier' => $entityIdentifier,
162-
'is_enabled' => [true, PDO::PARAM_BOOL],
163-
'is_federated' => [true, PDO::PARAM_BOOL],
164160
],
165161
$owner,
166162
);
@@ -190,6 +186,29 @@ public function findByEntityIdentifier(string $entityIdentifier, ?string $owner
190186
return $clientEntity;
191187
}
192188

189+
public function findFederatedByEntityIdentifier(
190+
string $entityIdentifier,
191+
?string $owner = null,
192+
): ?ClientEntityInterface {
193+
$clientEntity = $this->findByEntityIdentifier($entityIdentifier, $owner);
194+
195+
if (is_null($clientEntity)) {
196+
return null;
197+
}
198+
199+
if (
200+
is_null($clientEntity->getEntityIdentifier()) ||
201+
(! $clientEntity->isEnabled()) ||
202+
(! $clientEntity->isFederated()) ||
203+
(!is_array($clientEntity->getFederationJwks())) ||
204+
$clientEntity->isExpired()
205+
) {
206+
return null;
207+
}
208+
209+
return $clientEntity;
210+
}
211+
193212
private function addOwnerWhereClause(string $query, array $params, ?string $owner = null): array
194213
{
195214
if (isset($owner)) {
@@ -234,6 +253,47 @@ public function findAll(?string $owner = null): array
234253
return $clients;
235254
}
236255

256+
/**
257+
* @return \SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface[]
258+
* @throws \JsonException
259+
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
260+
*/
261+
public function findAllFederated(?string $owner = null): array
262+
{
263+
/**
264+
* @var string $query
265+
* @var array $params
266+
*/
267+
[$query, $params] = $this->addOwnerWhereClause(
268+
<<<EOS
269+
SELECT * FROM {$this->getTableName()}
270+
WHERE
271+
entity_identifier IS NOT NULL AND
272+
federation_jwks IS NOT NULL AND
273+
is_enabled = :is_enabled AND
274+
is_federated = :is_federated
275+
EOS,
276+
[
277+
'is_enabled' => [true, PDO::PARAM_BOOL],
278+
'is_federated' => [true, PDO::PARAM_BOOL],
279+
],
280+
$owner,
281+
);
282+
$stmt = $this->database->read(
283+
"$query ORDER BY name ASC",
284+
$params,
285+
);
286+
287+
$clients = [];
288+
289+
/** @var array $state */
290+
foreach ($stmt->fetchAll() as $state) {
291+
$clients[] = $this->clientEntityFactory->fromState($state);
292+
}
293+
294+
return $clients;
295+
}
296+
237297
/**
238298
* @return array{
239299
* numPages: int,
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Test\Module\oidc\unit\Controllers\Federation;
6+
7+
use PHPUnit\Framework\Attributes\CoversClass;
8+
use PHPUnit\Framework\MockObject\MockObject;
9+
use PHPUnit\Framework\TestCase;
10+
use SimpleSAML\Module\oidc\Controllers\Federation\SubordinateListingsController;
11+
use SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface;
12+
use SimpleSAML\Module\oidc\ModuleConfig;
13+
use SimpleSAML\Module\oidc\Repositories\ClientRepository;
14+
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
15+
use SimpleSAML\Module\oidc\Utils\Routes;
16+
use SimpleSAML\OpenID\Codebooks\ErrorsEnum;
17+
use SimpleSAML\OpenID\Codebooks\ParamsEnum;
18+
use Symfony\Component\HttpFoundation\ParameterBag;
19+
use Symfony\Component\HttpFoundation\Request;
20+
21+
#[CoversClass(SubordinateListingsController::class)]
22+
final class SubordinateListingsControllerTest extends TestCase
23+
{
24+
private MockObject $moduleConfigMock;
25+
private MockObject $clientRepositoryMock;
26+
private MockObject $routesMock;
27+
28+
private bool $isFederationEnabled;
29+
private MockObject $requestMock;
30+
private MockObject $requestQueryMock;
31+
32+
33+
protected function setUp(): void
34+
{
35+
$this->moduleConfigMock = $this->createMock(ModuleConfig::class);
36+
$this->clientRepositoryMock = $this->createMock(ClientRepository::class);
37+
$this->routesMock = $this->createMock(Routes::class);
38+
39+
$this->isFederationEnabled = true;
40+
41+
$this->requestMock = $this->createMock(Request::class);
42+
$this->requestQueryMock = $this->createMock(ParameterBag::class);
43+
$this->requestMock->query = $this->requestQueryMock;
44+
}
45+
46+
public function sut(
47+
?ModuleConfig $moduleConfig = null,
48+
?ClientRepository $clientRepository = null,
49+
?Routes $routes = null,
50+
?bool $federationEnabled = null,
51+
): SubordinateListingsController {
52+
$federationEnabled = $federationEnabled ?? $this->isFederationEnabled;
53+
$this->moduleConfigMock->method('getFederationEnabled')->willReturn($federationEnabled);
54+
55+
$moduleConfig = $moduleConfig ?? $this->moduleConfigMock;
56+
$clientRepository = $clientRepository ?? $this->clientRepositoryMock;
57+
$routes = $routes ?? $this->routesMock;
58+
59+
return new SubordinateListingsController(
60+
$moduleConfig,
61+
$clientRepository,
62+
$routes,
63+
);
64+
}
65+
66+
public function testCanConstruct(): void
67+
{
68+
$this->assertInstanceOf(SubordinateListingsController::class, $this->sut());
69+
}
70+
71+
public function testThrowsIfFederationNotEnabled(): void
72+
{
73+
$this->expectException(OidcServerException::class);
74+
$this->expectExceptionMessage('refused');
75+
76+
$this->sut(federationEnabled: false);
77+
}
78+
79+
public function testCanListFederatedEntities(): void
80+
{
81+
$client = $this->createMock(ClientEntityInterface::class);
82+
$client->method('getEntityIdentifier')->willReturn('entity-id');
83+
84+
$federatedEntities = [
85+
$client,
86+
];
87+
88+
$this->clientRepositoryMock->expects($this->once())->method('findAllFederated')
89+
->willReturn($federatedEntities);
90+
91+
$this->routesMock->expects($this->once())->method('newJsonResponse')
92+
->with([
93+
$client->getEntityIdentifier(),
94+
]);
95+
96+
$this->sut()->list($this->requestMock);
97+
}
98+
99+
public function testListReturnsErrorOnUnsuportedQueryParameter(): void
100+
{
101+
$this->requestQueryMock->method('all')->willReturn([
102+
ParamsEnum::EntityType->value => 'something',
103+
]);
104+
105+
$this->routesMock->expects($this->once())->method('newJsonErrorResponse')
106+
->with(ErrorsEnum::UnsupportedParameter->value);
107+
108+
$this->sut()->list($this->requestMock);
109+
}
110+
}

tests/unit/src/Repositories/ClientRepositoryTest.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ public function testCanFindByIdFromCache(): void
391391

392392
public function testCanFindByEntityIdentifier(): void
393393
{
394-
$client = self::getClient(id: 'clientId', entityId: 'entityId', isFederated: true);
394+
$client = self::getClient(id: 'clientId', entityId: 'entityId');
395395
$this->repository->add($client);
396396

397397
$this->clientEntityFactoryMock->expects($this->once())->method('fromState')->willReturn($client);
@@ -404,6 +404,46 @@ public function testCanFindByEntityIdentifier(): void
404404
$this->assertNull($this->repository->findByEntityIdentifier('nonExistingEntityId'));
405405
}
406406

407+
public function testCanFindFederatedByEntityIdentifier(): void
408+
{
409+
$client = self::getClient(id: 'clientId', entityId: 'entityId', isFederated: true, federationJwks: []);
410+
$this->repository->add($client);
411+
412+
$this->clientEntityFactoryMock->expects($this->once())->method('fromState')->willReturn($client);
413+
414+
$this->assertSame(
415+
$client,
416+
$this->repository->findFederatedByEntityIdentifier('entityId'),
417+
);
418+
419+
$this->assertNull($this->repository->findFederatedByEntityIdentifier('nonExistingEntityId'));
420+
}
421+
422+
public function testCanNotFindFederatedByEntityIdentifierIfMissingFederationAttributes(): void
423+
{
424+
$client = self::getClient(id: 'clientId', entityId: 'entityId');
425+
$this->repository->add($client);
426+
427+
$this->clientEntityFactoryMock->expects($this->atLeastOnce())->method('fromState')->willReturn($client);
428+
429+
$this->assertSame(
430+
$client,
431+
$this->repository->findByEntityIdentifier('entityId'),
432+
);
433+
434+
$this->assertNull($this->repository->findFederatedByEntityIdentifier('entityId'));
435+
}
436+
437+
public function testCanFindAllFederated(): void
438+
{
439+
$client = self::getClient(id: 'clientId', entityId: 'entityId', isFederated: true, federationJwks: []);
440+
$this->repository->add($client);
441+
442+
$this->clientEntityFactoryMock->expects($this->atLeastOnce())->method('fromState')->willReturn($client);
443+
444+
$this->assertCount(1, $this->repository->findAllFederated());
445+
}
446+
407447
public function testCanFindByEntityIdFromCache(): void
408448
{
409449
$protocolCacheMock = $this->createMock(ProtocolCache::class);
@@ -430,6 +470,7 @@ public static function getClient(
430470
?string $owner = null,
431471
?string $entityId = null,
432472
bool $isFederated = false,
473+
?array $federationJwks = null,
433474
): ClientEntityInterface {
434475
return new ClientEntity(
435476
identifier: $id,
@@ -443,6 +484,7 @@ public static function getClient(
443484
authSource: 'admin',
444485
owner: $owner,
445486
entityIdentifier: $entityId,
487+
federationJwks: $federationJwks,
446488
isFederated: $isFederated,
447489
);
448490
}

0 commit comments

Comments
 (0)