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 f65aa07c0..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,9 +536,9 @@ 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, $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..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,9 +418,9 @@ 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, $this->manager, $server); + ?? get_encrypted_fields_from_server($this->databaseName, $collectionName, $server); } $operation = isset($options['encryptedFields']) @@ -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/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..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 1513f4964..a332144cf 160000 --- a/tests/drivers-evergreen-tools +++ b/tests/drivers-evergreen-tools @@ -1 +1 @@ -Subproject commit 1513f4964048be964a598cf7255f14f1d34accb5 +Subproject commit a332144cfc785ab178be3d9d62e645cb79b5f81e 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