Skip to content

PHPLIB-1702: Always consult server encryptedFieldsMap when dropping collections with autoEncryption enabled #1745

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 13, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 5 additions & 2 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class Client

private WriteConcern $writeConcern;

private bool $autoEncryptionEnabled;
Copy link
Member

@GromNaN GromNaN Aug 13, 2025

Choose a reason for hiding this comment

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

@alcaeus @jmikola This new property will not be documented in Database and Collection options.

Copy link
Member Author

Choose a reason for hiding this comment

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

Created PHPC-2615 to allow us to fetch this from the Manager directly.


/**
* Constructs a new Client instance.
*
Expand Down Expand Up @@ -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']);
Copy link
Member Author

Choose a reason for hiding this comment

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

Noted that keyVaultNamespace is a required autoEncryption option.

phongo_manager_set_auto_encryption_opts doesn't seem to check about keyVaultNamespace being unset, so I assume we'd rely on libmongoc(rypt) raising an error since it's documented as required. In any event, it's sensible to check for it directly instead of just seeing if autoEncryption is set.


$driverOptions = array_diff_key($driverOptions, ['builderEncoder' => 1, 'typeMap' => 1]);

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
12 changes: 10 additions & 2 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -106,6 +107,8 @@ class Collection

private WriteConcern $writeConcern;

private bool $autoEncryptionEnabled;

/**
* Constructs new Collection instance.
*
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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'])
Expand Down
13 changes: 11 additions & 2 deletions src/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
use Traversable;

use function is_array;
use function is_bool;
use function sprintf;
use function strlen;
use function trigger_error;
Expand Down Expand Up @@ -82,6 +83,8 @@ class Database

private WriteConcern $writeConcern;

private bool $autoEncryptionEnabled;

/**
* Constructs new Database instance.
*
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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'])
Expand All @@ -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);
Expand Down
7 changes: 1 addition & 6 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Ideally, it seems like we'd want the Manager to be able to report whether autoEncryption is enabled.

return null;
}

$collectionInfoIterator = (new ListCollections($databaseName, ['filter' => ['name' => $collectionName]]))->execute($server);

foreach ($collectionInfoIterator as $collectionInfo) {
Expand Down
101 changes: 101 additions & 0 deletions tests/Functions/GetEncryptedFieldsFromServerFunctionalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

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 preg_quote;
use function str_repeat;

class GetEncryptedFieldsFromServerFunctionalTest extends FunctionalTestCase
{
private ClientEncryption $clientEncryption;
private Collection $keyVaultCollection;
private Database $database;

public function setUp(): void
{
parent::setUp();

$this->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);
}
}
2 changes: 1 addition & 1 deletion tests/drivers-evergreen-tools