From 97284ec042b43463feccee463b7a1f8e8d513dfd Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Fri, 8 Aug 2025 14:02:59 -0400 Subject: [PATCH 1/4] PHPLIB-1702: Always consult server encryptedFieldsMap when dropping collections --- src/Collection.php | 2 +- src/Database.php | 2 +- src/functions.php | 7 +- ...ncryptedFieldsFromServerFunctionalTest.php | 93 +++++++++++++++++++ 4 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php diff --git a/src/Collection.php b/src/Collection.php index f65aa07c0..4fb72eeb2 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -530,7 +530,7 @@ public function drop(array $options = []) if (! isset($options['encryptedFields'])) { $options['encryptedFields'] = get_encrypted_fields_from_driver($this->databaseName, $this->collectionName, $this->manager) - ?? get_encrypted_fields_from_server($this->databaseName, $this->collectionName, $this->manager, $server); + ?? get_encrypted_fields_from_server($this->databaseName, $this->collectionName, $server); } $operation = isset($options['encryptedFields']) diff --git a/src/Database.php b/src/Database.php index c0142915c..4ceb1240f 100644 --- a/src/Database.php +++ b/src/Database.php @@ -412,7 +412,7 @@ public function dropCollection(string $collectionName, array $options = []) if (! isset($options['encryptedFields'])) { $options['encryptedFields'] = get_encrypted_fields_from_driver($this->databaseName, $collectionName, $this->manager) - ?? get_encrypted_fields_from_server($this->databaseName, $collectionName, $this->manager, $server); + ?? get_encrypted_fields_from_server($this->databaseName, $collectionName, $server); } $operation = isset($options['encryptedFields']) diff --git a/src/functions.php b/src/functions.php index a445467ba..1c23d4ad8 100644 --- a/src/functions.php +++ b/src/functions.php @@ -201,13 +201,8 @@ function get_encrypted_fields_from_driver(string $databaseName, string $collecti * @see Database::dropCollection() * @return array|object|null */ -function get_encrypted_fields_from_server(string $databaseName, string $collectionName, Manager $manager, Server $server) +function get_encrypted_fields_from_server(string $databaseName, string $collectionName, Server $server) { - // No-op if the encryptedFieldsMap autoEncryption driver option was omitted - if ($manager->getEncryptedFieldsMap() === null) { - return null; - } - $collectionInfoIterator = (new ListCollections($databaseName, ['filter' => ['name' => $collectionName]]))->execute($server); foreach ($collectionInfoIterator as $collectionInfo) { diff --git a/tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php b/tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php new file mode 100644 index 000000000..26294cb7c --- /dev/null +++ b/tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php @@ -0,0 +1,93 @@ +skipIfClientSideEncryptionIsNotSupported(); + + if ($this->isStandalone()) { + $this->markTestSkipped('Queryable encryption requires replica sets'); + } + + $this->skipIfServerVersion('<', '7.0.0', 'Queryable encryption requires MongoDB 7.0 or later'); + + $client = static::createTestClient(); + + // Ensure the key vault collection is dropped before each test + $this->keyVaultCollection = $client->getCollection('keyvault', 'datakeys', ['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]); + $this->keyVaultCollection->drop(); + + $this->clientEncryption = $client->createClientEncryption([ + 'keyVaultNamespace' => $this->keyVaultCollection->getNamespace(), + 'kmsProviders' => ['local' => ['key' => new Binary(str_repeat("\0", 96)) ]], + ]); + + $this->database = $client->getDatabase($this->getDatabaseName()); + } + + public function tearDown(): void + { + $this->keyVaultCollection->drop(); + } + + /** @see https://jira.mongodb.org/browse/PHPLIB-1702 */ + public function testDatabaseDropCollectionConsultsEncryptedFieldsFromServer(): void + { + $originalNumCollections = iterator_count($this->database->listCollectionNames()); + + $this->database->createEncryptedCollection( + $this->getCollectionName(), + $this->clientEncryption, + 'local', + null, + ['encryptedFields' => ['fields' => []]], + ); + + // createEncryptedCollection should create three collections + $this->assertCount($originalNumCollections + 3, $this->database->listCollectionNames()); + + $this->database->dropCollection($this->getCollectionName()); + + $this->assertCount($originalNumCollections, $this->database->listCollectionNames()); + } + + /** @see https://jira.mongodb.org/browse/PHPLIB-1702 */ + public function testCollectionDropConsultsEncryptedFieldsFromServer(): void + { + $originalNumCollections = iterator_count($this->database->listCollectionNames()); + + $this->database->createEncryptedCollection( + $this->getCollectionName(), + $this->clientEncryption, + 'local', + null, + ['encryptedFields' => ['fields' => []]], + ); + + // createEncryptedCollection should create three collections + $this->assertCount($originalNumCollections + 3, $this->database->listCollectionNames()); + + $this->database->getCollection($this->getCollectionName())->drop(); + + $this->assertCount($originalNumCollections, $this->database->listCollectionNames()); + } +} From 8ef5fe67332623aa1eea10cf7ebc76103dd5be03 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Tue, 12 Aug 2025 17:34:15 -0400 Subject: [PATCH 2/4] Bump tests/drivers-evergreen-tools to a332144 Necessary to fix PyMongo compat with MongoDB 4.0 --- tests/drivers-evergreen-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/drivers-evergreen-tools b/tests/drivers-evergreen-tools index 1513f4964..a332144cf 160000 --- a/tests/drivers-evergreen-tools +++ b/tests/drivers-evergreen-tools @@ -1 +1 @@ -Subproject commit 1513f4964048be964a598cf7255f14f1d34accb5 +Subproject commit a332144cfc785ab178be3d9d62e645cb79b5f81e From 878e5cdc8a2f58f3867b9d0ac3133c68b379ab18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 13 Aug 2025 13:26:27 +0200 Subject: [PATCH 3/4] Detect metadata collections only when auto encryption is enabled on the client --- src/Client.php | 7 +++- src/Collection.php | 10 ++++- src/Database.php | 11 ++++- ...ncryptedFieldsFromServerFunctionalTest.php | 42 +++++++++++-------- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/Client.php b/src/Client.php index 32d064f64..0c2778235 100644 --- a/src/Client.php +++ b/src/Client.php @@ -83,6 +83,8 @@ class Client private WriteConcern $writeConcern; + private bool $autoEncryptionEnabled; + /** * Constructs a new Client instance. * @@ -134,6 +136,7 @@ public function __construct(?string $uri = null, array $uriOptions = [], array $ $this->uri = $uri ?? self::DEFAULT_URI; $this->builderEncoder = $driverOptions['builderEncoder'] ?? new BuilderEncoder(); $this->typeMap = $driverOptions['typeMap']; + $this->autoEncryptionEnabled = isset($driverOptions['autoEncryption']['keyVaultNamespace']); $driverOptions = array_diff_key($driverOptions, ['builderEncoder' => 1, 'typeMap' => 1]); @@ -258,7 +261,7 @@ public function dropDatabase(string $databaseName, array $options = []) */ public function getCollection(string $databaseName, string $collectionName, array $options = []): Collection { - $options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder]; + $options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder, 'autoEncryptionEnabled' => $this->autoEncryptionEnabled]; return new Collection($this->manager, $databaseName, $collectionName, $options); } @@ -273,7 +276,7 @@ public function getCollection(string $databaseName, string $collectionName, arra */ public function getDatabase(string $databaseName, array $options = []): Database { - $options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder]; + $options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder, 'autoEncryptionEnabled' => $this->autoEncryptionEnabled]; return new Database($this->manager, $databaseName, $options); } diff --git a/src/Collection.php b/src/Collection.php index 4fb72eeb2..714e684d4 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -77,6 +77,7 @@ use function array_key_exists; use function current; use function is_array; +use function is_bool; use function sprintf; use function strlen; use function trigger_error; @@ -106,6 +107,8 @@ class Collection private WriteConcern $writeConcern; + private bool $autoEncryptionEnabled; + /** * Constructs new Collection instance. * @@ -173,12 +176,17 @@ public function __construct(private Manager $manager, private string $databaseNa throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class); } + if (isset($options['autoEncryptionEnabled']) && ! is_bool($options['autoEncryptionEnabled'])) { + throw InvalidArgumentException::invalidType('"autoEncryptionEnabled" option', $options['autoEncryptionEnabled'], 'boolean'); + } + $this->builderEncoder = $options['builderEncoder'] ?? new BuilderEncoder(); $this->codec = $options['codec'] ?? null; $this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern(); $this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference(); $this->typeMap = $options['typeMap'] ?? self::DEFAULT_TYPE_MAP; $this->writeConcern = $options['writeConcern'] ?? $this->manager->getWriteConcern(); + $this->autoEncryptionEnabled = $options['autoEncryptionEnabled'] ?? false; } /** @@ -528,7 +536,7 @@ public function drop(array $options = []) $server = select_server_for_write($this->manager, $options); - if (! isset($options['encryptedFields'])) { + if ($this->autoEncryptionEnabled && ! isset($options['encryptedFields'])) { $options['encryptedFields'] = get_encrypted_fields_from_driver($this->databaseName, $this->collectionName, $this->manager) ?? get_encrypted_fields_from_server($this->databaseName, $this->collectionName, $server); } diff --git a/src/Database.php b/src/Database.php index 4ceb1240f..822363082 100644 --- a/src/Database.php +++ b/src/Database.php @@ -55,6 +55,7 @@ use Traversable; use function is_array; +use function is_bool; use function sprintf; use function strlen; use function trigger_error; @@ -82,6 +83,8 @@ class Database private WriteConcern $writeConcern; + private bool $autoEncryptionEnabled; + /** * Constructs new Database instance. * @@ -138,11 +141,16 @@ public function __construct(private Manager $manager, private string $databaseNa throw InvalidArgumentException::invalidType('"writeConcern" option', $options['writeConcern'], WriteConcern::class); } + if (isset($options['autoEncryptionEnabled']) && ! is_bool($options['autoEncryptionEnabled'])) { + throw InvalidArgumentException::invalidType('"autoEncryptionEnabled" option', $options['autoEncryptionEnabled'], 'boolean'); + } + $this->builderEncoder = $options['builderEncoder'] ?? new BuilderEncoder(); $this->readConcern = $options['readConcern'] ?? $this->manager->getReadConcern(); $this->readPreference = $options['readPreference'] ?? $this->manager->getReadPreference(); $this->typeMap = $options['typeMap'] ?? self::DEFAULT_TYPE_MAP; $this->writeConcern = $options['writeConcern'] ?? $this->manager->getWriteConcern(); + $this->autoEncryptionEnabled = $options['autoEncryptionEnabled'] ?? false; } /** @@ -410,7 +418,7 @@ public function dropCollection(string $collectionName, array $options = []) $options['writeConcern'] = $this->writeConcern; } - if (! isset($options['encryptedFields'])) { + if ($this->autoEncryptionEnabled && ! isset($options['encryptedFields'])) { $options['encryptedFields'] = get_encrypted_fields_from_driver($this->databaseName, $collectionName, $this->manager) ?? get_encrypted_fields_from_server($this->databaseName, $collectionName, $server); } @@ -439,6 +447,7 @@ public function getCollection(string $collectionName, array $options = []): Coll 'readPreference' => $this->readPreference, 'typeMap' => $this->typeMap, 'writeConcern' => $this->writeConcern, + 'autoEncryptionEnabled' => $this->autoEncryptionEnabled, ]; return new Collection($this->manager, $this->databaseName, $collectionName, $options); diff --git a/tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php b/tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php index 26294cb7c..47849c1ce 100644 --- a/tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php +++ b/tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php @@ -3,13 +3,14 @@ namespace MongoDB\Tests\Functions; use MongoDB\BSON\Binary; +use MongoDB\BSON\Regex; use MongoDB\Collection; use MongoDB\Database; use MongoDB\Driver\ClientEncryption; use MongoDB\Driver\WriteConcern; use MongoDB\Tests\FunctionalTestCase; -use function iterator_count; +use function preg_quote; use function str_repeat; class GetEncryptedFieldsFromServerFunctionalTest extends FunctionalTestCase @@ -30,30 +31,33 @@ public function setUp(): void $this->skipIfServerVersion('<', '7.0.0', 'Queryable encryption requires MongoDB 7.0 or later'); - $client = static::createTestClient(); + $encryptionOptions = [ + 'keyVaultNamespace' => 'keyvault.datakeys', + 'kmsProviders' => [ + 'local' => [ + 'key' => new Binary(str_repeat("\0", 96)), // 96-byte local master key + ], + ], + ]; + $client = static::createTestClient(driverOptions: ['autoEncryption' => $encryptionOptions]); // Ensure the key vault collection is dropped before each test $this->keyVaultCollection = $client->getCollection('keyvault', 'datakeys', ['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]); $this->keyVaultCollection->drop(); - $this->clientEncryption = $client->createClientEncryption([ - 'keyVaultNamespace' => $this->keyVaultCollection->getNamespace(), - 'kmsProviders' => ['local' => ['key' => new Binary(str_repeat("\0", 96)) ]], - ]); + $this->clientEncryption = $client->createClientEncryption($encryptionOptions); $this->database = $client->getDatabase($this->getDatabaseName()); } public function tearDown(): void { - $this->keyVaultCollection->drop(); + $this->keyVaultCollection?->drop(); } /** @see https://jira.mongodb.org/browse/PHPLIB-1702 */ public function testDatabaseDropCollectionConsultsEncryptedFieldsFromServer(): void { - $originalNumCollections = iterator_count($this->database->listCollectionNames()); - $this->database->createEncryptedCollection( $this->getCollectionName(), $this->clientEncryption, @@ -62,19 +66,16 @@ public function testDatabaseDropCollectionConsultsEncryptedFieldsFromServer(): v ['encryptedFields' => ['fields' => []]], ); - // createEncryptedCollection should create three collections - $this->assertCount($originalNumCollections + 3, $this->database->listCollectionNames()); + $this->assertCountCollections(3, $this->getCollectionName(), 'createEncryptedCollection should create three collections'); $this->database->dropCollection($this->getCollectionName()); - $this->assertCount($originalNumCollections, $this->database->listCollectionNames()); + $this->assertCountCollections(0, $this->getCollectionName()); } /** @see https://jira.mongodb.org/browse/PHPLIB-1702 */ public function testCollectionDropConsultsEncryptedFieldsFromServer(): void { - $originalNumCollections = iterator_count($this->database->listCollectionNames()); - $this->database->createEncryptedCollection( $this->getCollectionName(), $this->clientEncryption, @@ -83,11 +84,18 @@ public function testCollectionDropConsultsEncryptedFieldsFromServer(): void ['encryptedFields' => ['fields' => []]], ); - // createEncryptedCollection should create three collections - $this->assertCount($originalNumCollections + 3, $this->database->listCollectionNames()); + $this->assertCountCollections(3, $this->getCollectionName(), 'createEncryptedCollection should create three collections'); $this->database->getCollection($this->getCollectionName())->drop(); - $this->assertCount($originalNumCollections, $this->database->listCollectionNames()); + $this->assertCountCollections(0, $this->getCollectionName()); + } + + private function assertCountCollections(int $expected, $collectionName, string $message = ''): void + { + $collectionNames = $this->database->listCollectionNames([ + 'filter' => ['name' => new Regex(preg_quote($collectionName))], + ]); + $this->assertCount($expected, $collectionNames, $message); } } From bbc17002ae07a39b97102e6b7d5e821613deb403 Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 13 Aug 2025 09:54:14 -0400 Subject: [PATCH 4/4] Bump tests/specifications to 0aee4aa Necessary to fix change stream tests for server 8.2+ --- tests/specifications | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/specifications b/tests/specifications index 449d0397b..0aee4aad0 160000 --- a/tests/specifications +++ b/tests/specifications @@ -1 +1 @@ -Subproject commit 449d0397bbaf3d41d23f14453e4702e0251027b4 +Subproject commit 0aee4aad0bc6710a8fae5910c36d41b8a60a0688