Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 3 additions & 20 deletions config/schema/mongodb-1.0.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -179,26 +179,9 @@
<xsd:attribute name="tlsDisableOCSPEndpointCheck" type="xsd:boolean" use="optional" />
</xsd:complexType>

<xsd:complexType name="encrypted-fields-map">
<xsd:sequence>
<xsd:element name="encryptedFields" type="encrypted-fields" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>

<xsd:complexType name="encrypted-fields">
<xsd:sequence>
<xsd:element name="field" type="encrypted-field" minOccurs="1" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>

<xsd:complexType name="encrypted-field">
<xsd:sequence>
<xsd:element name="queries" type="encrypted-queries" minOccurs="0" maxOccurs="1" />
</xsd:sequence>
<xsd:attribute name="path" type="xsd:string" use="required" />
<xsd:attribute name="bsonType" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:simpleType name="encrypted-fields-map">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>

<xsd:complexType name="encrypted-queries">
<xsd:attribute name="queryType" type="xsd:string" use="required" />
Expand Down
34 changes: 22 additions & 12 deletions docs/encryption.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://www.mongodb.com/docs/manual/core/queryable-encryption/fundamentals/encrypt-and-query/>`_.
For more details, see the official MongoDB documentation:
`Encrypted Fields and Enabled Queries <https://www.mongodb.com/docs/manual/core/queryable-encryption/fundamentals/encrypt-and-query/>`_.

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::

Expand All @@ -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

Expand All @@ -117,9 +122,15 @@ For more details, see the official MongoDB documentation: `Encrypted Fields and
<doctrine:connection>
<doctrine:autoEncryption>
<doctrine:encryptedFieldsMap>
<doctrine:encryptedFields name="mydatabase.mycollection">
<doctrine:field path="sensitive_field" bsonType="string" />
</doctrine:encryptedFields>
<![CDATA[
{
"mydatabase.mycollection": [
"keyId": { "$binary": { "base64": "2CSosXLSTEKaYphcSnUuCw==", "subType": "04" } },
"path": "sensitive_field",
"bsonType": "string"
]
}
]]>
</doctrine:encryptedFieldsMap>
</doctrine:autoEncryption>
</doctrine:connection>
Expand All @@ -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' ] ],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For PHP, should we suggest people create a Binary instance?

Suggested change
'keyId' => ['$binary' => ['base64' => '2CSosXLSTEKaYphcSnUuCw==', 'subType' => '04' ] ],
'keyId' => new Binary(base64_decode('2CSosXLSTEKaYphcSnUuCw=='), Binary::TYPE_UUID),

On second thought after reviewing the rest of this PR, I'm guessing this format is created by the PHP dumper in the command that dumps the encryptedFieldsMap. Feel free to disregard in that case.

Copy link
Member Author

@GromNaN GromNaN Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BSON binary is not supported because I use json_encode on the value in DoctrineMongoDBExtension.

There is no function that is able to read a mix of BSON and RelaxedExtendedJSON in an array.

'bsonType' => 'string',
],
],
],
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<!-- <server name="DOCTRINE_MONGODB_ODM" value="/path/to/doctrine-mongodb-odm/lib" /> -->
<!-- <server name="DOCTRINE_MONGODB" value="/path/to/doctrine-mongodb/lib" /> -->
<!-- <server name="DOCTRINE_COMMON" value="/path/to/doctrine-common/lib" /> -->
<env name="DOCTRINE_MONGODB_SERVER" value="mongodb://localhost:27017"/>
<!-- Allow 1 direct deprecation until https://github.com/doctrine/DoctrineMongoDBBundle/pull/675 is merged -->
<env name="SYMFONY_DEPRECATIONS_HELPER" value="max[direct]=1"/>
</php>
Expand Down
34 changes: 16 additions & 18 deletions src/Command/DumpEncryptedFieldsMapCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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()]]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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) {
Expand Down
26 changes: 9 additions & 17 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,6 +19,8 @@
use function method_exists;
use function preg_match;

use const JSON_THROW_ON_ERROR;

/**
* FrameworkExtension configuration structure.
*/
Expand Down Expand Up @@ -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;
Expand All @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions src/DependencyInjection/DoctrineMongoDBExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down
Loading