Skip to content

Commit fa7cf81

Browse files
committed
Enhance encryption commands and documentation
- Updated COMMANDS.md to include the new `doctrine:encrypt:rotate-keys` command for key rotation, detailing its options and usage. - Improved the `doctrine:encrypt:status` command to list encrypted properties along with their effective config names. - Added a new method in AbstractCommand to retrieve encrypted properties with their config. - Updated service configuration to inject key paths into relevant commands. - Adjusted unit tests to reflect changes in output formatting for entity encryption status.
1 parent abdb5bc commit fa7cf81

5 files changed

Lines changed: 106 additions & 18 deletions

File tree

docs/COMMANDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Get the current database and encryption information:
2525
php bin/console doctrine:encrypt:status
2626
```
2727

28-
This returns the number of entities and the number of properties marked with the `Encrypted` attribute per entity.
28+
The command lists, for each entity, which properties are encrypted and under which **config** (e.g. `personal_data`, `financial_data`). It then shows a summary (how many entities have encrypted properties and the total encrypted property count) and, at the end, the **configured encryptor configs** in the project (config name, encryptor class such as Halite/Defuse, and which config is the default).
2929

3030
## Encrypt database
3131

src/Command/AbstractCommand.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ abstract class AbstractCommand extends Command
2222
* @param AttributeReader $attributeReader Reader for Encrypted attributes
2323
* @param DoctrineEncryptSubscriber $subscriber Encrypt/decrypt event subscriber
2424
* @param EncryptorInterface|null $defaultEncryptor Used by encrypt/decrypt database commands when using multi-config
25-
* @param EncryptorRegistry|null $encryptorRegistry Used by encrypt/decrypt database commands to resolve config names and encryptors
25+
* @param EncryptorRegistry|null $encryptorRegistry Used by encrypt/decrypt database commands and status command to resolve config names
2626
*/
2727
public function __construct(
2828
public EntityManagerInterface $entityManager,
@@ -110,6 +110,27 @@ protected function getEncryptionableProperties($entityMetaData): array
110110
return $properties;
111111
}
112112

113+
/**
114+
* Returns encrypted properties with their effective config name (resolves "default" to $defaultConfigName).
115+
*
116+
* @param object $entityMetaData Doctrine entity metadata (ClassMetadata)
117+
* @param string $defaultConfigName Default config name (e.g. from EncryptorRegistry::getDefaultName())
118+
* @return array<int, array{property: \ReflectionProperty, config: string}>
119+
*/
120+
protected function getEncryptionablePropertiesWithConfig($entityMetaData, string $defaultConfigName): array
121+
{
122+
$reflectionClass = new \ReflectionClass($entityMetaData->name);
123+
$result = [];
124+
foreach ($reflectionClass->getProperties() as $property) {
125+
$annotation = $this->attributeReader->getPropertyAnnotation($property, Encrypted::class);
126+
if ($annotation instanceof Encrypted) {
127+
$config = $annotation->config === 'default' ? $defaultConfigName : $annotation->config;
128+
$result[] = ['property' => $property, 'config' => $config];
129+
}
130+
}
131+
return $result;
132+
}
133+
113134
/**
114135
* Returns properties marked with Encrypted that belong to the given config.
115136
* When a property has config "default", it is included if $defaultConfigName === $configName.

src/Command/DoctrineEncryptStatusCommand.php

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
namespace Nowo\DoctrineEncryptBundle\Command;
44

5+
use Nowo\DoctrineEncryptBundle\Encryptors\EncryptorRegistry;
56
use Symfony\Component\Console\Attribute\AsCommand;
67
use Symfony\Component\Console\Input\InputInterface;
7-
// attributes
88
use Symfony\Component\Console\Output\OutputInterface;
99

1010
/**
11-
* Console command that lists entities and the count of encrypted properties per entity.
11+
* Console command that lists entities, their encrypted properties with config, and the configured encryptor configs.
1212
*/
1313
#[AsCommand(
1414
name: 'doctrine:encrypt:status',
@@ -19,38 +19,100 @@
1919
class DoctrineEncryptStatusCommand extends AbstractCommand
2020
{
2121
/**
22-
* Outputs each entity and its encrypted property count, then a total summary.
22+
* Config name => [path, encryptor_class]; used to show encryptor class in configured configs.
23+
*
24+
* @var array<string, array{path: string|null, encryptor_class: string}>
25+
*/
26+
private array $keyPaths;
27+
28+
/**
29+
* @param array<string, array{path: string|null, encryptor_class: string}> $keyPaths
30+
*/
31+
public function __construct(
32+
\Doctrine\ORM\EntityManagerInterface $entityManager,
33+
\Nowo\DoctrineEncryptBundle\Mapping\AttributeReader $attributeReader,
34+
\Nowo\DoctrineEncryptBundle\Subscribers\DoctrineEncryptSubscriber $subscriber,
35+
?\Nowo\DoctrineEncryptBundle\Encryptors\EncryptorInterface $defaultEncryptor = null,
36+
?EncryptorRegistry $encryptorRegistry = null,
37+
array $keyPaths = []
38+
) {
39+
parent::__construct($entityManager, $attributeReader, $subscriber, $defaultEncryptor, $encryptorRegistry);
40+
$this->keyPaths = $keyPaths;
41+
}
42+
43+
/**
44+
* Outputs each entity with its encrypted properties and config, then a summary and the configured configs.
2345
*
2446
* @param InputInterface $input Console input
2547
* @param OutputInterface $output Console output
2648
* @return int Command::SUCCESS
2749
*/
2850
protected function execute(InputInterface $input, OutputInterface $output): int
2951
{
30-
$metaDataArray = $this->entityManager->getMetadataFactory()->getAllMetadata();
52+
$defaultConfigName = $this->encryptorRegistry?->getDefaultName() ?? 'default';
3153

54+
$metaDataArray = $this->entityManager->getMetadataFactory()->getAllMetadata();
55+
$entitiesWithEncryption = 0;
3256
$totalCount = 0;
57+
3358
foreach ($metaDataArray as $metaData) {
3459
if (isset($metaData->isMappedSuperclass) && $metaData->isMappedSuperclass) {
3560
continue;
3661
}
3762

38-
$count = 0;
39-
$encryptedPropertiesCount = count($this->getEncryptionableProperties($metaData));
40-
if ($encryptedPropertiesCount > 0) {
41-
$totalCount += $encryptedPropertiesCount;
42-
$count += $encryptedPropertiesCount;
43-
}
63+
$propertiesWithConfig = $this->getEncryptionablePropertiesWithConfig($metaData, $defaultConfigName);
64+
$count = count($propertiesWithConfig);
4465

4566
if ($count > 0) {
46-
$output->writeln(sprintf('<info>%s</info> has <info>%d</info> properties which are encrypted.', $metaData->name, $count));
67+
$entitiesWithEncryption++;
68+
$totalCount += $count;
69+
$output->writeln(sprintf('<info>%s</info> has <info>%d</info> encrypted property(ies):', $metaData->name, $count));
70+
foreach ($propertiesWithConfig as ['property' => $property, 'config' => $config]) {
71+
$output->writeln(sprintf(' - <comment>%s</comment> (config: <comment>%s</comment>)', $property->getName(), $config));
72+
}
4773
} else {
4874
$output->writeln(sprintf('<info>%s</info> has no properties which are encrypted.', $metaData->name));
4975
}
5076
}
5177

5278
$output->writeln('');
53-
$output->writeln(sprintf('<info>%d</info> entities found which are containing <info>%d</info> encrypted properties.', count($metaDataArray), $totalCount));
79+
$output->writeln(sprintf(
80+
'<info>%d</info> entit(y/ies) with encryption, <info>%d</info> encrypted properties in total (out of <info>%d</info> entities).',
81+
$entitiesWithEncryption,
82+
$totalCount,
83+
count($metaDataArray)
84+
));
85+
86+
$this->outputConfiguredConfigs($output, $defaultConfigName);
87+
5488
return AbstractCommand::SUCCESS;
5589
}
90+
91+
private function outputConfiguredConfigs(OutputInterface $output, string $defaultConfigName): void
92+
{
93+
$output->writeln('');
94+
$output->writeln('<info>Configured encryptor configs:</info>');
95+
96+
if ($this->encryptorRegistry === null) {
97+
$output->writeln(' (registry not available)');
98+
return;
99+
}
100+
101+
$configNames = $this->encryptorRegistry->getConfigNames();
102+
// Exclude the 'default' alias when there are other configs (so we don't list "default" and "personal_data" when default is personal_data)
103+
$namesToShow = array_values(array_filter($configNames, static fn (string $name): bool => $name !== 'default' || count($configNames) === 1));
104+
105+
if (count($namesToShow) === 0) {
106+
$output->writeln(' (none)');
107+
return;
108+
}
109+
110+
$keyPaths = $this->keyPaths;
111+
foreach ($namesToShow as $name) {
112+
$encryptorClass = $keyPaths[$name]['encryptor_class'] ?? null;
113+
$label = $encryptorClass ? sprintf('%s (%s)', $name, $encryptorClass) : $name;
114+
$defaultLabel = $name === $defaultConfigName ? ' [default]' : '';
115+
$output->writeln(sprintf(' - %s%s', $label, $defaultLabel));
116+
}
117+
}
56118
}

src/Resources/config/services.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ services:
2222
$kernel: '@kernel'
2323
$keyPaths: '%nowo_doctrine_encrypt.key_paths%'
2424

25+
Nowo\DoctrineEncryptBundle\Command\DoctrineEncryptStatusCommand:
26+
arguments:
27+
$encryptorRegistry: '@nowo_doctrine_encrypt.encryptor_registry'
28+
$keyPaths: '%nowo_doctrine_encrypt.key_paths%'
29+
2530
nowo_doctrine_encrypt.encryptor_registry:
2631
class: Nowo\DoctrineEncryptBundle\Encryptors\EncryptorRegistry
2732
arguments: [[], 'default']

tests/Unit/Command/DoctrineEncryptStatusCommandTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public function testExecuteOutputsSummaryWithEntityAndPropertyCounts(): void
8686

8787
$this->assertSame(0, $tester->getStatusCode());
8888
$display = $tester->getDisplay();
89-
$this->assertMatchesRegularExpression('/\d+ entities found which are containing \d+ encrypted properties\./', $display);
89+
$this->assertMatchesRegularExpression('/\d+ entit\(y\/ies\) with encryption, \d+ encrypted properties in total/', $display);
9090
}
9191

9292
public function testExecuteSkipsMappedSuperclassAndOutputsOthers(): void
@@ -137,8 +137,8 @@ public function testExecuteWithNoEntitiesOutputsZeroSummary(): void
137137

138138
$this->assertSame(0, $tester->getStatusCode());
139139
$display = $tester->getDisplay();
140-
$this->assertStringContainsString('0 entities found', $display);
141-
$this->assertStringContainsString('0 encrypted properties', $display);
140+
$this->assertStringContainsString('0 entit(y/ies) with encryption', $display);
141+
$this->assertStringContainsString('0 encrypted properties in total', $display);
142142
}
143143

144144
public function testExecuteOutputsMultipleEntitiesWithEncryptedProperties(): void
@@ -169,6 +169,6 @@ public function testExecuteOutputsMultipleEntitiesWithEncryptedProperties(): voi
169169
$display = $tester->getDisplay();
170170
$this->assertStringContainsString(User::class, $display);
171171
$this->assertStringContainsString(EntityWithConfigAlias::class, $display);
172-
$this->assertMatchesRegularExpression('/\d+ entities found which are containing \d+ encrypted properties\./', $display);
172+
$this->assertMatchesRegularExpression('/\d+ entit\(y\/ies\) with encryption, \d+ encrypted properties in total/', $display);
173173
}
174174
}

0 commit comments

Comments
 (0)