diff --git a/config/schema/mongodb-1.0.xsd b/config/schema/mongodb-1.0.xsd
index 026d714b..3d336ff6 100644
--- a/config/schema/mongodb-1.0.xsd
+++ b/config/schema/mongodb-1.0.xsd
@@ -179,26 +179,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/docs/encryption.rst b/docs/encryption.rst
index 5be85afd..13c6bc7b 100644
--- a/docs/encryption.rst
+++ b/docs/encryption.rst
@@ -92,7 +92,12 @@ This setting is **recommended** for improved security and performance.
specified collection, the client downloads the server-side remote schema for
the collection and uses it instead.
-For more details, see the official MongoDB documentation: `Encrypted Fields and Enabled Queries `_.
+For more details, see the official MongoDB documentation:
+`Encrypted Fields and Enabled Queries `_.
+
+Note that there is no ``fields`` key in the configuration of each collection
+for the bundle configuration. Instead, you directly specify the list of
+encrypted fields as an array under the collection namespace.
.. tabs::
@@ -106,9 +111,9 @@ For more details, see the official MongoDB documentation: `Encrypted Fields and
autoEncryption:
encryptedFieldsMap:
"mydatabase.mycollection":
- fields:
- - path: "sensitive_field"
- bsonType: "string"
+ - keyId: { $binary: { base64: 2CSosXLSTEKaYphcSnUuCw==, subType: '04' } }
+ path: "sensitive_field"
+ bsonType: "string"
.. group-tab:: XML
@@ -117,9 +122,15 @@ For more details, see the official MongoDB documentation: `Encrypted Fields and
-
-
-
+
@@ -135,11 +146,10 @@ For more details, see the official MongoDB documentation: `Encrypted Fields and
->autoEncryption([
'encryptedFieldsMap' => [
'mydatabase.mycollection' => [
- 'fields' => [
- [
- 'path' => 'sensitive_field',
- 'bsonType' => 'string',
- ],
+ [
+ 'path' => 'sensitive_field',
+ 'keyId' => ['$binary' => ['base64' => '2CSosXLSTEKaYphcSnUuCw==', 'subType' => '04' ] ],
+ 'bsonType' => 'string',
],
],
],
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 50eaaae1..2ef3bdec 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -20,6 +20,7 @@
+
diff --git a/src/Command/DumpEncryptedFieldsMapCommand.php b/src/Command/DumpEncryptedFieldsMapCommand.php
index f2c999dc..25b0d630 100644
--- a/src/Command/DumpEncryptedFieldsMapCommand.php
+++ b/src/Command/DumpEncryptedFieldsMapCommand.php
@@ -6,7 +6,7 @@
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
-use Doctrine\ODM\MongoDB\Utility\EncryptedFieldsMapGenerator;
+use MongoDB\BSON\PackedArray;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -16,10 +16,7 @@
use Symfony\Component\Yaml\Dumper;
use Symfony\Contracts\Service\ServiceCollectionInterface;
-use function array_combine;
-use function array_keys;
-use function array_map;
-use function array_values;
+use function json_decode;
use function json_encode;
use function sprintf;
use function var_export;
@@ -63,24 +60,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$dumper = new Dumper();
foreach ($this->documentManagers as $name => $documentManager) {
- $generator = new EncryptedFieldsMapGenerator($documentManager->getMetadataFactory());
- $encryptedFieldsMap = $generator->getEncryptedFieldsMap();
+ $encryptedFieldsMap = [];
+ foreach ($documentManager->getMetadataFactory()->getAllMetadata() as $metadata) {
+ $database = $documentManager->getDocumentDatabase($metadata->getName());
+ $collectionInfoIterator = $database->listCollections(['filter' => ['name' => $metadata->getCollection()]]);
+
+ foreach ($collectionInfoIterator as $collectionInfo) {
+ if ($collectionInfo['options']['encryptedFields'] ?? null) {
+ $encryptedFieldsMap[$this->getDocumentNamespace($metadata, $database->getDatabaseName())] = $collectionInfo['options']['encryptedFields'];
+ }
+ }
+ }
if (empty($encryptedFieldsMap)) {
continue;
}
- $encryptedFieldsMap = array_combine(
- // Convert class names in keys to their full namespaces
- array_map(
- fn (string $fqcn): string => $this->getDocumentNamespace(
- $documentManager->getClassMetadata($fqcn),
- $documentManager->getConfiguration()->getDefaultDB(),
- ),
- array_keys($encryptedFieldsMap),
- ),
- array_values($encryptedFieldsMap),
- );
+ foreach ($encryptedFieldsMap as $ns => $encryptedFields) {
+ $encryptedFieldsMap[$ns] = json_decode(PackedArray::fromPHP($encryptedFields['fields'])->toRelaxedExtendedJSON(), true);
+ }
$io->section(sprintf('Dumping encrypted fields map for document manager "%s"', $name));
switch ($format) {
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 17e483a3..b4b6a2b7 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -11,7 +11,6 @@
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
-use function array_is_list;
use function count;
use function in_array;
use function is_array;
@@ -20,6 +19,8 @@
use function method_exists;
use function preg_match;
+use const JSON_THROW_ON_ERROR;
+
/**
* FrameworkExtension configuration structure.
*/
@@ -401,20 +402,8 @@ private function addConnectionsSection(ArrayNodeDefinition $rootNode): void
->useAttributeAsKey('name', false)
->beforeNormalization()
->always(static function ($v) {
- if (isset($v['encryptedFields']) && is_array($v['encryptedFields'])) {
- $encryptedFields = $v['encryptedFields'];
- if (! array_is_list($encryptedFields)) {
- $encryptedFields = [$encryptedFields];
- }
-
- $v = [];
- foreach ($encryptedFields as $field) {
- if (is_array($field['field'] ?? null) && ! array_is_list($field['field'])) {
- $field['field'] = [$field['field']];
- }
-
- $v[$field['name'] ?? ''] = $field['field'] ?? [];
- }
+ if (is_string($v)) {
+ return json_decode($v, true, 512, JSON_THROW_ON_ERROR);
}
return $v;
@@ -424,13 +413,16 @@ private function addConnectionsSection(ArrayNodeDefinition $rootNode): void
->children()
->scalarNode('path')->isRequired()->cannotBeEmpty()->end()
->scalarNode('bsonType')->isRequired()->cannotBeEmpty()->end()
+ ->variableNode('keyId')->isRequired()->cannotBeEmpty()->end()
->arrayNode('queries')
->children()
->scalarNode('queryType')->isRequired()->cannotBeEmpty()->end()
- ->integerNode('min')->end()
- ->integerNode('max')->end()
+ ->variableNode('min')->end()
+ ->variableNode('max')->end()
->integerNode('sparsity')->end()
+ ->integerNode('precision')->end()
->integerNode('trimFactor')->end()
+ ->integerNode('contention')->end()
->end()
->end()
->end()
diff --git a/src/DependencyInjection/DoctrineMongoDBExtension.php b/src/DependencyInjection/DoctrineMongoDBExtension.php
index 595e5147..66b6ca75 100644
--- a/src/DependencyInjection/DoctrineMongoDBExtension.php
+++ b/src/DependencyInjection/DoctrineMongoDBExtension.php
@@ -27,6 +27,7 @@
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
use Doctrine\Persistence\Proxy;
use InvalidArgumentException;
+use MongoDB\BSON\Document as BsonDocument;
use MongoDB\Client;
use ProxyManager\Proxy\LazyLoadingInterface;
use Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension;
@@ -55,6 +56,7 @@
use function in_array;
use function interface_exists;
use function is_dir;
+use function json_encode;
use function method_exists;
use function sprintf;
@@ -538,6 +540,12 @@ private function normalizeAutoEncryption(array $autoEncryption, string $defaultD
$autoEncryption['keyVaultNamespace'] ??= $defaultDB . '.datakeys';
+ if (isset($autoEncryption['encryptedFieldsMap'])) {
+ foreach ($autoEncryption['encryptedFieldsMap'] as &$value) {
+ $value = (new Definition(BsonDocument::class))->setFactory([BsonDocument::class, 'fromJSON'])->setArguments([json_encode(['fields' => $value])]);
+ }
+ }
+
return $autoEncryption;
}
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
index 47613d78..dae2c3a0 100644
--- a/tests/DependencyInjection/ConfigurationTest.php
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -147,27 +147,108 @@ public function testFullConfiguration(array $config): void
'bypassAutoEncryption' => true,
'bypassQueryAnalysis' => true,
'encryptedFieldsMap' => [
+ 'encrypted.RangeTypes' => [
+ [
+ 'keyId' => ['$binary' => ['base64' => 'lhZHItpvRkqXevh4Wtqg/g==', 'subType' => '04']],
+ 'path' => 'intField',
+ 'bsonType' => 'int',
+ 'queries' => ['queryType' => 'range', 'contention' => 8, 'min' => 5, 'max' => 10],
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'qd9PEKIPTE2J30ev29lMpQ==', 'subType' => '04']],
+ 'path' => 'floatField',
+ 'bsonType' => 'double',
+ 'queries' => ['queryType' => 'range', 'contention' => 8, 'min' => 5.5, 'max' => 10.5, 'precision' => 1],
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'zVLg8CF4RSSu4xn7x7dOyQ==', 'subType' => '04']],
+ 'path' => 'decimalField',
+ 'bsonType' => 'decimal',
+ 'queries' => [
+ 'queryType' => 'range',
+ 'contention' => 8,
+ 'min' => ['$numberDecimal' => '0.1'],
+ 'max' => ['$numberDecimal' => '0.2'],
+ 'precision' => 2,
+ ],
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'ySdd8lZ2QBqnwKPJTp/yLA==', 'subType' => '04']],
+ 'path' => 'immutableDateField',
+ 'bsonType' => 'date',
+ 'queries' => [
+ 'queryType' => 'range',
+ 'contention' => 8,
+ 'min' => ['$date' => '2000-01-01T00:00:00Z'],
+ 'max' => ['$date' => '2100-01-01T00:00:00Z'],
+ ],
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'NWKI+DyES/OlNkUbJbWJ9w==', 'subType' => '04']],
+ 'path' => 'dateField',
+ 'bsonType' => 'date',
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'wiiv+0K/QAquyEq3HDxRKw==', 'subType' => '04']],
+ 'path' => 'binField',
+ 'bsonType' => 'binData',
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => '2CSosXLSTEKaYphcSnUuCw==', 'subType' => '04']],
+ 'path' => 'timestampField',
+ 'bsonType' => 'timestamp',
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'h3H6HdG3T5CK+Z2yQ4Ho+Q==', 'subType' => '04']],
+ 'path' => 'hashField',
+ 'bsonType' => 'object',
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'X78UZZ/HTX2wLw4K3uG42w==', 'subType' => '04']],
+ 'path' => 'collectionField',
+ 'bsonType' => 'objectId',
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'LugQL/ZXTJOl856Yacmkwg==', 'subType' => '04']],
+ 'path' => 'boolField',
+ 'bsonType' => 'bool',
+ ],
+ ],
'encrypted.patients' => [
[
- 'path' => 'patientRecord.ssn',
- 'bsonType' => 'string',
- 'queries' => ['queryType' => 'equality'],
+ 'keyId' => ['$binary' => ['base64' => 'GH25/XvYSaCgTUQLAo1hQw==', 'subType' => '04']],
+ 'path' => 'pathologies',
+ 'bsonType' => 'array',
],
[
+ 'keyId' => ['$binary' => ['base64' => 'krVWyFlNTUOaGFMfk+s7UA==', 'subType' => '04']],
'path' => 'patientRecord.billing',
'bsonType' => 'object',
],
[
+ 'keyId' => ['$binary' => ['base64' => 'X1ZaSI1GSAKnZ+sPGcmYBA==', 'subType' => '04']],
'path' => 'patientRecord.billingAmount',
'bsonType' => 'int',
- 'queries' => ['queryType' => 'range', 'min' => 100, 'max' => 2000, 'sparsity' => 1, 'trimFactor' => 4],
+ 'queries' => [
+ 'queryType' => 'range',
+ 'contention' => 8,
+ 'min' => 100,
+ 'max' => 2000,
+ 'sparsity' => 1,
+ 'trimFactor' => 4,
+ ],
],
],
- 'encrypted.users' => [
+ 'encrypted.client' => [
[
- 'path' => 'email',
+ 'keyId' => ['$binary' => ['base64' => 'I0Aw18vnRGWzVS1t3uejpQ==', 'subType' => '04']],
+ 'path' => 'name',
'bsonType' => 'string',
- 'queries' => ['queryType' => 'equality'],
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'XSPRK3vaTLmMZr9IEj/qwQ==', 'subType' => '04']],
+ 'path' => 'clientCards',
+ 'bsonType' => 'array',
],
],
],
@@ -491,7 +572,7 @@ public static function provideNormalizeOptions(): Generator
],
];
- // Encrypted Field Map normalization from XML tags
+ // Encrypted Field Map can be a JSON string in a
yield [
[
'connection' => [
@@ -500,38 +581,40 @@ public static function provideNormalizeOptions(): Generator
'id' => 'foo',
'autoEncryption' => [
'kmsProvider' => ['type' => 'local', 'key' => '1234567890123456789012345678901234567890123456789012345678901234'],
- 'encryptedFieldsMap' => [
- 'encryptedFields' => [
- [
- 'name' => 'encrypted.patients',
- 'field' => [
- [
- 'path' => 'patientRecord.ssn',
- 'bsonType' => 'string',
- 'queries' => ['queryType' => 'equality'],
- ],
- [
- 'path' => 'patientRecord.billing',
- 'bsonType' => 'object',
- ],
- [
- 'path' => 'patientRecord.billingAmount',
- 'bsonType' => 'int',
- 'queries' => ['queryType' => 'range', 'min' => 100, 'max' => 2000, 'sparsity' => 1, 'trimFactor' => 4],
- ],
- ],
- ],
- [
- 'name' => 'encrypted.users',
- 'field' =>
- [
- 'path' => 'email',
- 'bsonType' => 'string',
- 'queries' => ['queryType' => 'equality'],
- ],
- ],
+ 'encryptedFieldsMap' => <<<'JSON'
+ {
+ "encrypted.patients": [
+ {
+ "keyId": { "$binary": { "base64": "GH25/XvYSaCgTUQLAo1hQw==", "subType": "04" } },
+ "path": "pathologies",
+ "bsonType": "array"
+ },
+ {
+ "keyId": { "$binary": { "base64": "krVWyFlNTUOaGFMfk+s7UA==", "subType": "04" } },
+ "path": "patientRecord.billing",
+ "bsonType": "object"
+ },
+ {
+ "keyId": { "$binary": { "base64": "X1ZaSI1GSAKnZ+sPGcmYBA==", "subType": "04" } },
+ "path": "patientRecord.billingAmount",
+ "bsonType": "int",
+ "queries": { "queryType": "range", "contention": 8, "min": 100, "max": 2000, "sparsity": 1, "trimFactor": 4 }
+ }
],
- ],
+ "encrypted.client": [
+ {
+ "keyId": { "$binary": { "base64": "I0Aw18vnRGWzVS1t3uejpQ==", "subType": "04" } },
+ "path": "name",
+ "bsonType": "string"
+ },
+ {
+ "keyId": { "$binary": { "base64": "XSPRK3vaTLmMZr9IEj/qwQ==", "subType": "04" } },
+ "path": "clientCards",
+ "bsonType": "array"
+ }
+ ]
+ }
+ JSON,
],
],
],
@@ -545,25 +628,39 @@ public static function provideNormalizeOptions(): Generator
'encryptedFieldsMap' => [
'encrypted.patients' => [
[
- 'path' => 'patientRecord.ssn',
- 'bsonType' => 'string',
- 'queries' => ['queryType' => 'equality'],
+ 'keyId' => ['$binary' => ['base64' => 'GH25/XvYSaCgTUQLAo1hQw==', 'subType' => '04']],
+ 'path' => 'pathologies',
+ 'bsonType' => 'array',
],
[
+ 'keyId' => ['$binary' => ['base64' => 'krVWyFlNTUOaGFMfk+s7UA==', 'subType' => '04']],
'path' => 'patientRecord.billing',
'bsonType' => 'object',
],
[
+ 'keyId' => ['$binary' => ['base64' => 'X1ZaSI1GSAKnZ+sPGcmYBA==', 'subType' => '04']],
'path' => 'patientRecord.billingAmount',
'bsonType' => 'int',
- 'queries' => ['queryType' => 'range', 'min' => 100, 'max' => 2000, 'sparsity' => 1, 'trimFactor' => 4],
+ 'queries' => [
+ 'queryType' => 'range',
+ 'contention' => 8,
+ 'min' => 100,
+ 'max' => 2000,
+ 'sparsity' => 1,
+ 'trimFactor' => 4,
+ ],
],
],
- 'encrypted.users' => [
+ 'encrypted.client' => [
[
- 'path' => 'email',
+ 'keyId' => ['$binary' => ['base64' => 'I0Aw18vnRGWzVS1t3uejpQ==', 'subType' => '04']],
+ 'path' => 'name',
'bsonType' => 'string',
- 'queries' => ['queryType' => 'equality'],
+ ],
+ [
+ 'keyId' => ['$binary' => ['base64' => 'XSPRK3vaTLmMZr9IEj/qwQ==', 'subType' => '04']],
+ 'path' => 'clientCards',
+ 'bsonType' => 'array',
],
],
],
diff --git a/tests/DependencyInjection/Fixtures/config/xml/full.xml b/tests/DependencyInjection/Fixtures/config/xml/full.xml
index 65cdbdb6..c4a19374 100644
--- a/tests/DependencyInjection/Fixtures/config/xml/full.xml
+++ b/tests/DependencyInjection/Fixtures/config/xml/full.xml
@@ -7,8 +7,8 @@
http://symfony.com/schema/dic/doctrine/odm/mongodb http://symfony.com/schema/dic/doctrine/odm/mongodb/mongodb-1.0.xsd">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
setHydratorNamespace('SymfonyTests\Doctrine');
$config->setMetadataDriverImpl(new AttributeDriver($paths));
$config->setMetadataCache(new ArrayAdapter());
+ $uri = getenv('DOCTRINE_MONGODB_SERVER') ?: 'mongodb://localhost:27017';
- return DocumentManager::create(null, $config);
+ return DocumentManager::create(new Client($uri), $config);
}
}