From e155b892991036723ce167f6ed51e99bda1f6218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20FAYARD-LE=20BARZIC?= Date: Wed, 8 Oct 2025 22:29:56 +0200 Subject: [PATCH] feat(elasticsearch): add SSL options for Elasticsearch configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Frédéric FAYARD-LE BARZIC --- CHANGELOG.md | 6 ++ .../ApiPlatformExtension.php | 2 + .../Compiler/ElasticsearchClientPass.php | 8 ++ .../DependencyInjection/Configuration.php | 12 +++ .../Compiler/ElasticsearchClientPassTest.php | 102 ++++++++++++++++++ .../DependencyInjection/ConfigurationTest.php | 53 +++++++++ 6 files changed, 183 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ad8a2945d..a5531a7bda0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +* [21aa2572d](https://github.com/api-platform/core/commit/21aa2572d8fef2b3f05f7307c51348a6c9767e45) feat(elasticsearch): add SSL options for Elasticsearch configuration (#4059) + ## v4.2.2 ### Bug fixes diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index b59db5afd69..f2b9da89b29 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -907,6 +907,8 @@ private function registerElasticsearchConfiguration(ContainerBuilder $container, $container->registerForAutoconfiguration(RequestBodySearchCollectionExtensionInterface::class) ->addTag('api_platform.elasticsearch.request_body_search_extension.collection'); $container->setParameter('api_platform.elasticsearch.hosts', $config['elasticsearch']['hosts']); + $container->setParameter('api_platform.elasticsearch.ssl_ca_bundle', $config['elasticsearch']['ssl_ca_bundle']); + $container->setParameter('api_platform.elasticsearch.ssl_verification', $config['elasticsearch']['ssl_verification']); $loader->load('elasticsearch.php'); } diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/ElasticsearchClientPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/ElasticsearchClientPass.php index 574f35f5d75..7a429febe1f 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/ElasticsearchClientPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/ElasticsearchClientPass.php @@ -56,6 +56,14 @@ public function process(ContainerBuilder $container): void } } + if ($sslCaBundle = $container->getParameter('api_platform.elasticsearch.ssl_ca_bundle')) { + $clientConfiguration['CABundle'] = $sslCaBundle; + } + + if (false === $container->getParameter('api_platform.elasticsearch.ssl_verification')) { + $clientConfiguration['SSLVerification'] = false; + } + $clientDefinition = $container->getDefinition('api_platform.elasticsearch.client'); if (!$clientConfiguration) { diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index 4b96eadf555..83cf3330c0f 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -473,6 +473,10 @@ private function addElasticsearchSection(ArrayNodeDefinition $rootNode): void ->arrayNode('elasticsearch') ->canBeEnabled() ->addDefaultsIfNotSet() + ->validate() + ->ifTrue(static fn (array $v): bool => null !== ($v['ssl_ca_bundle'] ?? null) && false === ($v['ssl_verification'] ?? true)) + ->thenInvalid('The "ssl_ca_bundle" and "ssl_verification: false" options cannot be used together. Either provide a CA bundle path or disable SSL verification, not both.') + ->end() ->children() ->booleanNode('enabled') ->defaultFalse() @@ -497,6 +501,14 @@ private function addElasticsearchSection(ArrayNodeDefinition $rootNode): void ->defaultValue([]) ->prototype('scalar')->end() ->end() + ->scalarNode('ssl_ca_bundle') + ->defaultNull() + ->info('Path to the SSL CA bundle file for Elasticsearch SSL verification.') + ->end() + ->booleanNode('ssl_verification') + ->defaultTrue() + ->info('Enable or disable SSL verification for Elasticsearch connections.') + ->end() ->end() ->end() ->end(); diff --git a/tests/Symfony/Bundle/DependencyInjection/Compiler/ElasticsearchClientPassTest.php b/tests/Symfony/Bundle/DependencyInjection/Compiler/ElasticsearchClientPassTest.php index 1376fcd0be9..8d1a2c4a1c3 100644 --- a/tests/Symfony/Bundle/DependencyInjection/Compiler/ElasticsearchClientPassTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/Compiler/ElasticsearchClientPassTest.php @@ -69,6 +69,8 @@ public function testProcess(): void $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); $containerBuilderProphecy->getParameter('api_platform.elasticsearch.enabled')->willReturn(true)->shouldBeCalled(); $containerBuilderProphecy->getParameter('api_platform.elasticsearch.hosts')->willReturn(['http://localhost:9200'])->shouldBeCalled(); + $containerBuilderProphecy->getParameter('api_platform.elasticsearch.ssl_ca_bundle')->willReturn(null)->shouldBeCalled(); + $containerBuilderProphecy->getParameter('api_platform.elasticsearch.ssl_verification')->willReturn(true)->shouldBeCalled(); $containerBuilderProphecy->has('logger')->willReturn(true)->shouldBeCalled(); $containerBuilderProphecy->getDefinition('api_platform.elasticsearch.client')->willReturn($clientDefinitionProphecy->reveal())->shouldBeCalled(); @@ -89,6 +91,8 @@ public function testProcessWithoutConfiguration(): void $containerBuilderProphecy = $this->prophesize(ContainerBuilder::class); $containerBuilderProphecy->getParameter('api_platform.elasticsearch.enabled')->willReturn(true)->shouldBeCalled(); $containerBuilderProphecy->getParameter('api_platform.elasticsearch.hosts')->willReturn([])->shouldBeCalled(); + $containerBuilderProphecy->getParameter('api_platform.elasticsearch.ssl_ca_bundle')->willReturn(null)->shouldBeCalled(); + $containerBuilderProphecy->getParameter('api_platform.elasticsearch.ssl_verification')->willReturn(true)->shouldBeCalled(); $containerBuilderProphecy->has('logger')->willReturn(false)->shouldBeCalled(); $containerBuilderProphecy->getDefinition('api_platform.elasticsearch.client')->willReturn($clientDefinitionProphecy->reveal())->shouldBeCalled(); @@ -102,4 +106,102 @@ public function testProcessWithElasticsearchDisabled(): void (new ElasticsearchClientPass())->process($containerBuilderProphecy->reveal()); } + + public function testProcessWithSslCaBundle(): void + { + $clientBuilder = class_exists(\Elasticsearch\ClientBuilder::class) + // ES v7 + ? \Elasticsearch\ClientBuilder::class + // ES v8 and up + : \Elastic\Elasticsearch\ClientBuilder::class; + + $clientDefinition = $this->createMock(Definition::class); + $clientDefinition->expects($this->once()) + ->method('setFactory') + ->with([$clientBuilder, 'fromConfig']) + ->willReturnSelf(); + + $clientDefinition->expects($this->once()) + ->method('setArguments') + ->with($this->callback(function ($arguments) { + $config = $arguments[0]; + + return isset($config['hosts']) + && $config['hosts'] === ['https://localhost:9200'] + && isset($config['CABundle']) + && '/path/to/ca-bundle.crt' === $config['CABundle'] + && isset($config['logger']) + && $config['logger'] instanceof Reference; + })) + ->willReturnSelf(); + + $containerBuilder = $this->createMock(ContainerBuilder::class); + $containerBuilder->method('getParameter') + ->willReturnMap([ + ['api_platform.elasticsearch.enabled', true], + ['api_platform.elasticsearch.hosts', ['https://localhost:9200']], + ['api_platform.elasticsearch.ssl_ca_bundle', '/path/to/ca-bundle.crt'], + ['api_platform.elasticsearch.ssl_verification', true], + ]); + + $containerBuilder->expects($this->once()) + ->method('has') + ->with('logger') + ->willReturn(true); + + $containerBuilder->expects($this->once()) + ->method('getDefinition') + ->with('api_platform.elasticsearch.client') + ->willReturn($clientDefinition); + + (new ElasticsearchClientPass())->process($containerBuilder); + } + + public function testProcessWithSslVerificationDisabled(): void + { + $clientBuilder = class_exists(\Elasticsearch\ClientBuilder::class) + ? \Elasticsearch\ClientBuilder::class + : \Elastic\Elasticsearch\ClientBuilder::class; + + $clientDefinition = $this->createMock(Definition::class); + $clientDefinition->expects($this->once()) + ->method('setFactory') + ->with([$clientBuilder, 'fromConfig']) + ->willReturnSelf(); + + $clientDefinition->expects($this->once()) + ->method('setArguments') + ->with($this->callback(function ($arguments) { + $config = $arguments[0]; + + return isset($config['hosts']) + && $config['hosts'] === ['https://localhost:9200'] + && isset($config['SSLVerification']) + && false === $config['SSLVerification'] + && isset($config['logger']) + && $config['logger'] instanceof Reference; + })) + ->willReturnSelf(); + + $containerBuilder = $this->createMock(ContainerBuilder::class); + $containerBuilder->method('getParameter') + ->willReturnMap([ + ['api_platform.elasticsearch.enabled', true], + ['api_platform.elasticsearch.hosts', ['https://localhost:9200']], + ['api_platform.elasticsearch.ssl_ca_bundle', null], + ['api_platform.elasticsearch.ssl_verification', false], + ]); + + $containerBuilder->expects($this->once()) + ->method('has') + ->with('logger') + ->willReturn(true); + + $containerBuilder->expects($this->once()) + ->method('getDefinition') + ->with('api_platform.elasticsearch.client') + ->willReturn($clientDefinition); + + (new ElasticsearchClientPass())->process($containerBuilder); + } } diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 76264c21278..e3c4e91add8 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -135,6 +135,8 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'elasticsearch' => [ 'enabled' => false, 'hosts' => [], + 'ssl_ca_bundle' => null, + 'ssl_verification' => true, ], 'oauth' => [ 'enabled' => false, @@ -439,4 +441,55 @@ public function testOpenApiTags(): void $this->assertEquals(['name' => 'test3', 'description' => null], $config['openapi']['tags'][1]); } + + public function testElasticsearchSslCaBundleConfiguration(): void + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'elasticsearch' => [ + 'enabled' => true, + 'hosts' => ['https://localhost:9200'], + 'ssl_ca_bundle' => '/path/to/ca-bundle.crt', + ], + ], + ]); + + $this->assertTrue($config['elasticsearch']['enabled']); + $this->assertSame('/path/to/ca-bundle.crt', $config['elasticsearch']['ssl_ca_bundle']); + $this->assertTrue($config['elasticsearch']['ssl_verification']); + } + + public function testElasticsearchSslVerificationDisabled(): void + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'elasticsearch' => [ + 'enabled' => true, + 'hosts' => ['https://localhost:9200'], + 'ssl_verification' => false, + ], + ], + ]); + + $this->assertTrue($config['elasticsearch']['enabled']); + $this->assertFalse($config['elasticsearch']['ssl_verification']); + $this->assertNull($config['elasticsearch']['ssl_ca_bundle']); + } + + public function testElasticsearchSslCaBundleAndVerificationDisabledMutuallyExclusive(): void + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The "ssl_ca_bundle" and "ssl_verification: false" options cannot be used together. Either provide a CA bundle path or disable SSL verification, not both.'); + + $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'elasticsearch' => [ + 'enabled' => true, + 'hosts' => ['https://localhost:9200'], + 'ssl_ca_bundle' => '/path/to/ca-bundle.crt', + 'ssl_verification' => false, + ], + ], + ]); + } }