Skip to content

Commit 7ac04b5

Browse files
committed
[LiveComponent] Allow configuring secret for fingerprints and checksums
Allow to configure a dedicated secret (used in FingerprintCalculator and LiveComonentHydrator) Suggested by #2453 Inspired by #56840
1 parent c3ee75b commit 7ac04b5

File tree

4 files changed

+145
-4
lines changed

4 files changed

+145
-4
lines changed

src/LiveComponent/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.23.0
4+
5+
- Allow configuring the secret used to compute fingerprints and checksums.
6+
37
## 2.22.0
48

59
- Remove CSRF tokens - rely on same-origin/CORS instead

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
namespace Symfony\UX\LiveComponent\DependencyInjection;
1313

1414
use Symfony\Component\AssetMapper\AssetMapperInterface;
15+
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
16+
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
17+
use Symfony\Component\Config\Definition\ConfigurationInterface;
18+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1519
use Symfony\Component\DependencyInjection\ChildDefinition;
1620
use Symfony\Component\DependencyInjection\ContainerBuilder;
1721
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -57,7 +61,7 @@
5761
*
5862
* @internal
5963
*/
60-
final class LiveComponentExtension extends Extension implements PrependExtensionInterface
64+
final class LiveComponentExtension extends Extension implements PrependExtensionInterface, ConfigurationInterface
6165
{
6266
public const TEMPLATES_MAP_FILENAME = 'live_components_twig_templates.map';
6367

@@ -93,16 +97,19 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
9397
}
9498
);
9599

100+
$configuration = $this->getConfiguration($configs, $container);
101+
$config = $this->processConfiguration($configuration, $configs);
102+
96103
$container->registerForAutoconfiguration(HydrationExtensionInterface::class)
97104
->addTag(LiveComponentBundle::HYDRATION_EXTENSION_TAG);
98105

99106
$container->register('ux.live_component.component_hydrator', LiveComponentHydrator::class)
100107
->setArguments([
101-
tagged_iterator(LiveComponentBundle::HYDRATION_EXTENSION_TAG),
108+
new TaggedIteratorArgument(LiveComponentBundle::HYDRATION_EXTENSION_TAG),
102109
new Reference('property_accessor'),
103110
new Reference('ux.live_component.metadata_factory'),
104111
new Reference('serializer', ContainerInterface::NULL_ON_INVALID_REFERENCE),
105-
'%kernel.secret%',
112+
$config['secret'], // defaults to '%kernel.secret%'
106113
])
107114
;
108115

@@ -236,7 +243,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
236243

237244
$container->register('ux.live_component.deterministic_id_calculator', DeterministicTwigIdCalculator::class);
238245
$container->register('ux.live_component.fingerprint_calculator', FingerprintCalculator::class)
239-
->setArguments(['%kernel.secret%']);
246+
->setArguments([$config['secret']]); // default to %kernel.secret%
240247

241248
$container->setAlias(ComponentValidatorInterface::class, ComponentValidator::class);
242249

@@ -258,6 +265,35 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
258265
->addTag('kernel.cache_warmer');
259266
}
260267

268+
public function getConfigTreeBuilder(): TreeBuilder
269+
{
270+
$treeBuilder = new TreeBuilder('live_component');
271+
$rootNode = $treeBuilder->getRootNode();
272+
\assert($rootNode instanceof ArrayNodeDefinition);
273+
274+
$rootNode
275+
->addDefaultsIfNotSet()
276+
->children()
277+
->scalarNode('secret')
278+
->info('The secret used to compute fingerprints and checksums')
279+
->beforeNormalization()
280+
->ifString()
281+
->then(trim(...))
282+
->end()
283+
->cannotBeEmpty()
284+
->defaultValue('%kernel.secret%')
285+
->end()
286+
->end()
287+
;
288+
289+
return $treeBuilder;
290+
}
291+
292+
public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
293+
{
294+
return $this;
295+
}
296+
261297
private function isAssetMapperAvailable(ContainerBuilder $container): bool
262298
{
263299
if (!interface_exists(AssetMapperInterface::class)) {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Symfony\UX\LiveComponent\Tests\Unit\DependencyInjection;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
7+
use Symfony\Component\Config\Definition\Processor;
8+
use Symfony\UX\LiveComponent\DependencyInjection\LiveComponentExtension;
9+
10+
class LiveComponentConfigurationTest extends TestCase
11+
{
12+
public function testDefaultSecret()
13+
{
14+
$processor = new Processor();
15+
$config = $processor->processConfiguration(new LiveComponentExtension(), []);
16+
17+
$this->assertEquals('%kernel.secret%', $config['secret']);
18+
}
19+
20+
public function testEmptySecretThrows()
21+
{
22+
$this->expectException(InvalidConfigurationException::class);
23+
$this->expectExceptionMessage('The path "live_component.secret" cannot contain an empty value, but got null.');
24+
25+
$processor = new Processor();
26+
$config = $processor->processConfiguration(new LiveComponentExtension(), [
27+
'live_component' => [
28+
'secret' => null,
29+
],
30+
]);
31+
}
32+
33+
public function testCustomSecret()
34+
{
35+
$processor = new Processor();
36+
$config = $processor->processConfiguration(new LiveComponentExtension(), [
37+
'live_component' => [
38+
'secret' => 'my_secret',
39+
],
40+
]);
41+
42+
$this->assertEquals('my_secret', $config['secret']);
43+
}
44+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace Symfony\UX\LiveComponent\Tests\Unit\DependencyInjection;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Symfony\Component\DependencyInjection\ContainerBuilder;
7+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
8+
use Symfony\UX\LiveComponent\DependencyInjection\LiveComponentExtension;
9+
10+
class LiveComponentExtensionTest extends TestCase
11+
{
12+
public function testKernelSecretIsUsedByDefault(): void
13+
{
14+
$container = $this->createContainer();
15+
$container->registerExtension(new LiveComponentExtension());
16+
$container->loadFromExtension('live_component', []);
17+
$this->compileContainer($container);
18+
19+
$this->assertSame('%kernel.secret%', $container->getDefinition('ux.live_component.component_hydrator')->getArgument(4));
20+
$this->assertSame('%kernel.secret%', $container->getDefinition('ux.live_component.fingerprint_calculator')->getArgument(0));
21+
}
22+
23+
public function testCustomSecretIsUsedInDefinition(): void
24+
{
25+
$container = $this->createContainer();
26+
$container->registerExtension(new LiveComponentExtension());
27+
$container->loadFromExtension('live_component', [
28+
'secret' => 'custom_secret',
29+
]);
30+
$this->compileContainer($container);
31+
32+
$this->assertSame('custom_secret', $container->getDefinition('ux.live_component.component_hydrator')->getArgument(4));
33+
$this->assertSame('custom_secret', $container->getDefinition('ux.live_component.fingerprint_calculator')->getArgument(0));
34+
}
35+
36+
private function createContainer(): ContainerBuilder
37+
{
38+
$container = new ContainerBuilder(new ParameterBag([
39+
'kernel.cache_dir' => __DIR__,
40+
'kernel.project_dir' => __DIR__,
41+
'kernel.charset' => 'UTF-8',
42+
'kernel.debug' => false,
43+
'kernel.bundles' => [],
44+
'kernel.bundles_metadata' => [],
45+
]));
46+
47+
return $container;
48+
}
49+
50+
private function compileContainer(ContainerBuilder $container): void
51+
{
52+
$container->getCompilerPassConfig()->setOptimizationPasses([]);
53+
$container->getCompilerPassConfig()->setRemovingPasses([]);
54+
$container->getCompilerPassConfig()->setAfterRemovingPasses([]);
55+
$container->compile();
56+
}
57+
}

0 commit comments

Comments
 (0)