Skip to content

Commit 33ba9a9

Browse files
authored
Reject metadata push when IdP discovery entry is missing English name (#1957)
* Reject metadata push when IdP discovery entry is missing English name A discovery entry without name_en was silently dropped, which could leave an IdP with no discoveries and cause a WAYF crash. Now throws InvalidDiscoveryException, which propagates to ConnectionsController as HTTP 400. Fixes #1865. * Include entity ID and entry index in InvalidDiscoveryException message
1 parent 14bb0db commit 33ba9a9

5 files changed

Lines changed: 86 additions & 7 deletions

File tree

src/OpenConext/EngineBlock/Metadata/Entity/Assembler/DiscoveryAssembler.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
namespace OpenConext\EngineBlock\Metadata\Entity\Assembler;
2020

21+
use OpenConext\EngineBlock\Exception\InvalidDiscoveryException;
2122
use OpenConext\EngineBlock\Metadata\Discovery;
2223
use OpenConext\EngineBlock\Metadata\Logo;
2324
use OpenConext\EngineBlockBundle\Localization\LanguageSupportProvider;
@@ -41,15 +42,24 @@ public function assembleDiscoveries(stdClass $connection): array
4142
return [];
4243
}
4344

45+
$entityId = $connection->name ?? 'unknown';
4446
$discoveries = [];
45-
foreach ($connection->metadata->discoveries as $discovery) {
47+
foreach ($connection->metadata->discoveries as $index => $discovery) {
4648
$names = $this->extractLocalizedFields($discovery, 'name');
4749
$keywords = $this->extractLocalizedFields($discovery, 'keywords');
4850
$logo = $this->assembleLogo($discovery);
4951

50-
if (isset($names['en'])) {
51-
$discoveries[] = Discovery::create($names, $keywords, $logo);
52+
if (!isset($names['en'])) {
53+
throw new InvalidDiscoveryException(
54+
sprintf(
55+
'Discovery entry #%d for entity "%s" is missing a required English name (name_en). ' .
56+
'Refusing to process metadata push.',
57+
$index,
58+
$entityId
59+
)
60+
);
5261
}
62+
$discoveries[] = Discovery::create($names, $keywords, $logo);
5363
}
5464

5565
return empty($discoveries) ? [] : ['discoveries' => $discoveries];

tests/functional/OpenConext/EngineBlockBundle/Controller/Api/ConnectionsControllerTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,39 @@ public function pushing_entity_with_valid_manipulation_code_succeeds()
396396
$this->assertStatusCode(Response::HTTP_OK, $client);
397397
}
398398

399+
#[Group('Api')]
400+
#[Group('Connections')]
401+
#[Group('MetadataPush')]
402+
#[Test]
403+
public function pushing_idp_with_discovery_missing_english_name_returns_bad_request(): void
404+
{
405+
$client = $this->createAuthorizedClient();
406+
407+
$payload = '{"connections":{"idp1":{
408+
"allow_all_entities":true,
409+
"allowed_connections":[],
410+
"metadata":{
411+
"name":{"en":"Test IdP","nl":"Test IdP nl"},
412+
"discoveries":[{"name_nl":"Ontbrekend Engels"}],
413+
"SingleSignOnService":[{
414+
"Binding":"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
415+
"Location":"https://idp.example.com/sso"
416+
}]
417+
},
418+
"name":"https://idp.example.com",
419+
"state":"prodaccepted",
420+
"type":"saml20-idp"
421+
}}}';
422+
423+
$client->request('POST', 'https://engine-api.dev.openconext.local/api/connections', [], [], [], $payload);
424+
425+
$this->assertStatusCode(Response::HTTP_BAD_REQUEST, $client);
426+
$this->assertJson($client->getResponse()->getContent());
427+
$isContentTypeJson = $client->getResponse()->headers->contains('Content-Type', 'application/json');
428+
$this->assertTrue($isContentTypeJson, 'Response should have Content-Type: application/json header');
429+
$this->assertStringContainsString('name_en', $client->getResponse()->getContent());
430+
}
431+
399432
public static function invalidHttpMethodProvider()
400433
{
401434
return [

tests/integration/OpenConext/EngineBlock/Metadata/Entity/Assembler/DiscoveryAssemblerTest.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
namespace OpenConext\EngineBlock\Tests;
2020

21+
use OpenConext\EngineBlock\Exception\InvalidDiscoveryException;
2122
use OpenConext\EngineBlock\Metadata\Discovery;
2223
use OpenConext\EngineBlock\Metadata\Entity\Assembler\DiscoveryAssembler;
2324
use OpenConext\EngineBlock\Metadata\Logo;
@@ -84,8 +85,11 @@ public function testAssembleDiscoveriesProcessesValidDiscovery(): void
8485
$this->assertEquals(100, $discovery->getLogo()->height);
8586
}
8687

87-
public function testAssembleDiscoveriesSkipsDiscoveryWithoutEnglishName(): void
88+
public function testAssembleDiscoveriesThrowsWhenDiscoveryMissingEnglishName(): void
8889
{
90+
$this->expectException(InvalidDiscoveryException::class);
91+
$this->expectExceptionMessage('missing a required English name');
92+
8993
$connection = new stdClass();
9094
$connection->metadata = new stdClass();
9195
$connection->metadata->discoveries = [
@@ -95,9 +99,7 @@ public function testAssembleDiscoveriesSkipsDiscoveryWithoutEnglishName(): void
9599
])
96100
];
97101

98-
$result = $this->discoveryAssembler->assembleDiscoveries($connection);
99-
100-
$this->assertEquals([], $result);
102+
$this->discoveryAssembler->assembleDiscoveries($connection);
101103
}
102104

103105
public function testAssembleDiscoveriesWithTrimmedValues(): void

tests/integration/OpenConext/EngineBlock/Metadata/Entity/Assembler/PushMetadataAssemblerTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
use Mockery as m;
2222
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
23+
use OpenConext\EngineBlock\Exception\InvalidDiscoveryException;
2324
use OpenConext\EngineBlock\Metadata\AttributeReleasePolicy;
2425
use OpenConext\EngineBlock\Metadata\Discovery;
2526
use OpenConext\EngineBlock\Metadata\Entity\Assembler\PushMetadataAssembler;
@@ -569,6 +570,15 @@ public function test_it_assembles_metadata_without_discoveries_details()
569570
$this->assertSame('https://engine.dev.openconext.local/images/logo.png?v=dev', $idp->logo->url);
570571
}
571572

573+
public function test_it_rejects_idp_with_discovery_missing_english_name(): void
574+
{
575+
$this->expectException(InvalidDiscoveryException::class);
576+
$this->expectExceptionMessage('missing a required English name');
577+
578+
$input = $this->readFixture('metadata_idp_discovery_missing_english.json');
579+
$this->assembler->assemble($input);
580+
}
581+
572582
private function readFixture(string $file): object
573583
{
574584
return json_decode(file_get_contents(__DIR__ . '/fixtures/'. basename($file)), false);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"2d96e27a-76cf-4ca2-ac70-ece5d4c49525": {
3+
"name": "https:\/\/serviceregistry.dev.openconext.local\/simplesaml\/module.php\/saml\/sp\/metadata.php\/default-idp",
4+
"state": "prodaccepted",
5+
"type": "saml20-idp",
6+
"metadata": {
7+
"discoveries": [
8+
{
9+
"name_nl": "Ontbrekend Engels"
10+
}
11+
],
12+
"SingleSignOnService": [
13+
{
14+
"Binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
15+
"Location": "https:\/\/mujina-idp.dev.openConext.local\/SingleSignOnService"
16+
}
17+
],
18+
"name": {
19+
"en": "Dummy IdP",
20+
"nl": "Dummy IdP nl"
21+
}
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)