diff --git a/src/Client.php b/src/Client.php index dce14446e..159f0caf2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -82,6 +82,8 @@ class Client private WriteConcern $writeConcern; + private bool $autoEncryptionEnabled; + /** * Constructs a new Client instance. * @@ -133,6 +135,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]); @@ -270,7 +273,7 @@ public function dropDatabase(string $databaseName, array $options = []): void */ 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); } @@ -285,7 +288,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 04a61981c..1b51efe66 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -75,6 +75,7 @@ use function array_key_exists; use function current; use function is_array; +use function is_bool; use function strlen; class Collection @@ -100,6 +101,8 @@ class Collection private WriteConcern $writeConcern; + private bool $autoEncryptionEnabled; + /** * Constructs new Collection instance. * @@ -167,12 +170,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; } /** @@ -511,9 +519,9 @@ public function drop(array $options = []): void $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, $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 265772b53..aba7c03a5 100644 --- a/src/Database.php +++ b/src/Database.php @@ -54,6 +54,7 @@ use Throwable; use function is_array; +use function is_bool; use function strlen; class Database @@ -77,6 +78,8 @@ class Database private WriteConcern $writeConcern; + private bool $autoEncryptionEnabled; + /** * Constructs new Database instance. * @@ -133,11 +136,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; } /** @@ -372,9 +380,9 @@ public function dropCollection(string $collectionName, array $options = []): voi $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, $this->manager, $server); + ?? get_encrypted_fields_from_server($this->databaseName, $collectionName, $server); } $operation = isset($options['encryptedFields']) @@ -401,6 +409,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/src/functions.php b/src/functions.php index aca396723..d33760c81 100644 --- a/src/functions.php +++ b/src/functions.php @@ -198,13 +198,8 @@ function get_encrypted_fields_from_driver(string $databaseName, string $collecti * @see Collection::drop() * @see Database::dropCollection() */ -function get_encrypted_fields_from_server(string $databaseName, string $collectionName, Manager $manager, Server $server): array|object|null +function get_encrypted_fields_from_server(string $databaseName, string $collectionName, Server $server): array|object|null { - // 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..47849c1ce --- /dev/null +++ b/tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php @@ -0,0 +1,101 @@ +skipIfClientSideEncryptionIsNotSupported(); + + if ($this->isStandalone()) { + $this->markTestSkipped('Queryable encryption requires replica sets'); + } + + $this->skipIfServerVersion('<', '7.0.0', 'Queryable encryption requires MongoDB 7.0 or later'); + + $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($encryptionOptions); + + $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 + { + $this->database->createEncryptedCollection( + $this->getCollectionName(), + $this->clientEncryption, + 'local', + null, + ['encryptedFields' => ['fields' => []]], + ); + + $this->assertCountCollections(3, $this->getCollectionName(), 'createEncryptedCollection should create three collections'); + + $this->database->dropCollection($this->getCollectionName()); + + $this->assertCountCollections(0, $this->getCollectionName()); + } + + /** @see https://jira.mongodb.org/browse/PHPLIB-1702 */ + public function testCollectionDropConsultsEncryptedFieldsFromServer(): void + { + $this->database->createEncryptedCollection( + $this->getCollectionName(), + $this->clientEncryption, + 'local', + null, + ['encryptedFields' => ['fields' => []]], + ); + + $this->assertCountCollections(3, $this->getCollectionName(), 'createEncryptedCollection should create three collections'); + + $this->database->getCollection($this->getCollectionName())->drop(); + + $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); + } +} diff --git a/tests/drivers-evergreen-tools b/tests/drivers-evergreen-tools index 32fd8eb5d..a332144cf 160000 --- a/tests/drivers-evergreen-tools +++ b/tests/drivers-evergreen-tools @@ -1 +1 @@ -Subproject commit 32fd8eb5de7be062c5750619c232189263371f98 +Subproject commit a332144cfc785ab178be3d9d62e645cb79b5f81e