Skip to content

PHPLIB-1071: Do not create ECC collection and check wire version for QEv2 #1084

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
May 31, 2023
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
1 change: 1 addition & 0 deletions src/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ public function createCollection(string $collectionName, array $options = [])
* @return array A tuple containing the command result document from creating the collection and the modified "encryptedFields" option
* @throws InvalidArgumentException for parameter/option parsing errors
* @throws CreateEncryptedCollectionException for any errors creating data keys or creating the collection
* @throws UnsupportedException if Queryable Encryption is not supported by the selected server
*/
public function createEncryptedCollection(string $collectionName, ClientEncryption $clientEncryption, string $kmsProvider, ?array $masterKey, array $options): array
{
Expand Down
13 changes: 11 additions & 2 deletions src/Operation/CreateEncryptedCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
use MongoDB\Driver\Server;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Exception\UnsupportedException;

use function array_key_exists;
use function is_array;
use function is_object;
use function MongoDB\server_supports_feature;

/**
* Create an encrypted collection.
Expand All @@ -44,6 +46,9 @@
*/
class CreateEncryptedCollection implements Executable
{
/** @var integer */
private static $wireVersionForQueryableEncryptionV2 = 21;

/** @var CreateCollection */
private $createCollection;

Expand Down Expand Up @@ -81,13 +86,12 @@ public function __construct(string $databaseName, string $collectionName, array

$this->createCollection = new CreateCollection($databaseName, $collectionName, $options);

/** @psalm-var array{eccCollection?: ?string, ecocCollection?: ?string, escCollection?: ?string} */
/** @psalm-var array{ecocCollection?: ?string, escCollection?: ?string} */
$encryptedFields = (array) $options['encryptedFields'];
$enxcolOptions = ['clusteredIndex' => ['key' => ['_id' => 1], 'unique' => true]];

$this->createMetadataCollections = [
new CreateCollection($databaseName, $encryptedFields['escCollection'] ?? 'enxcol_.' . $collectionName . '.esc', $enxcolOptions),
new CreateCollection($databaseName, $encryptedFields['eccCollection'] ?? 'enxcol_.' . $collectionName . '.ecc', $enxcolOptions),
new CreateCollection($databaseName, $encryptedFields['ecocCollection'] ?? 'enxcol_.' . $collectionName . '.ecoc', $enxcolOptions),
];

Expand Down Expand Up @@ -150,9 +154,14 @@ public function createDataKeys(ClientEncryption $clientEncryption, string $kmsPr
* @see Executable::execute()
* @return array|object Command result document from creating the encrypted collection
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
* @throws UnsupportedException if the server does not support Queryable Encryption
*/
public function execute(Server $server)
{
if (! server_supports_feature($server, self::$wireVersionForQueryableEncryptionV2)) {
throw new UnsupportedException('Driver support of Queryable Encryption is incompatible with server. Upgrade server to use Queryable Encryption.');
}

foreach ($this->createMetadataCollections as $createMetadataCollection) {
$createMetadataCollection->execute($server);
}
Expand Down
3 changes: 1 addition & 2 deletions src/Operation/DropEncryptedCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,11 @@ public function __construct(string $databaseName, string $collectionName, array
throw InvalidArgumentException::invalidType('"encryptedFields" option', $options['encryptedFields'], ['array', 'object']);
}

/** @psalm-var array{eccCollection?: ?string, ecocCollection?: ?string, escCollection?: ?string} */
/** @psalm-var array{ecocCollection?: ?string, escCollection?: ?string} */
$encryptedFields = (array) $options['encryptedFields'];

$this->dropMetadataCollections = [
new DropCollection($databaseName, $encryptedFields['escCollection'] ?? 'enxcol_.' . $collectionName . '.esc'),
new DropCollection($databaseName, $encryptedFields['eccCollection'] ?? 'enxcol_.' . $collectionName . '.ecc'),
new DropCollection($databaseName, $encryptedFields['ecocCollection'] ?? 'enxcol_.' . $collectionName . '.ecoc'),
];

Expand Down
4 changes: 4 additions & 0 deletions tests/SpecTests/ClientSideEncryptionSpecTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,15 @@ public function setUp(): void
/**
* Assert that the expected and actual command documents match.
*
* Note: this method may modify the $expected object.
*
* @param stdClass $expected Expected command document
* @param stdClass $actual Actual command document
*/
public static function assertCommandMatches(stdClass $expected, stdClass $actual): void
{
static::assertCommandOmittedFields($expected, $actual);

static::assertDocumentsMatch($expected, $actual);
}

Expand Down
23 changes: 23 additions & 0 deletions tests/SpecTests/FunctionalTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use function json_encode;
use function MongoDB\BSON\fromJSON;
use function MongoDB\BSON\toPHP;
use function property_exists;
use function sprintf;
use function version_compare;

Expand Down Expand Up @@ -95,6 +96,28 @@ public static function assertDocumentsMatch($expectedDocument, $actualDocument,
static::assertThat($actualDocument, $constraint, $message);
}

/**
* Assert omitted fields in command documents.
*
* Note: this method may modify the $expected object.
*
* @see https://github.com/mongodb/specifications/blob/master/source/transactions/tests/README.rst#null-values
*/
protected static function assertCommandOmittedFields(stdClass $expected, stdClass $actual): void
{
foreach ($expected as $key => $value) {
if ($value === null) {
static::assertObjectNotHasAttribute($key, $actual);
unset($expected->{$key});
continue;
}

if ($value instanceof stdClass && property_exists($actual, $key) && $actual->{$key} instanceof stdClass) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Note: the previous code for this function was:

if ($value instanceof stdClass) {
    static::assertObjectHasAttribute($key, $actual);
    static::assertInstanceOf(stdClass::class, $actual->{$key});
    static::assertCommandOmittedFields($value, $actual->{$key});
}

This caused multiple test failures in the getMore.json test, which asserts that the getMore field of the outgoing command matches { "$$type": "long" }. The actual value in that case is an integer, which was problematic for assertInstanceOf() above.

Rather than rewrite assertCommandOmittedFields() to skip special operations (i.e. keys starting with "$$"), which is only defined in the CSFLE legacy test format, I opted to just NOP and avoid recursion. This keeps assertCommandOmittedFields() focused on it's original purpose of only asserting omitted fields and allows the caller to handle everything else (likely via assertDocumentsMatch()).

static::assertCommandOmittedFields($value, $actual->{$key});
}
}
}

/**
* Assert data within the outcome collection.
*/
Expand Down
8 changes: 2 additions & 6 deletions tests/SpecTests/TransactionsSpecTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public function tearDown(): void
*
* Note: this method may modify the $expected object.
*
* @see https://github.com/mongodb/specifications/blob/master/source/transactions/tests/README.rst#command-started-events
* @param stdClass $expected Expected command document
* @param stdClass $actual Actual command document
*/
Expand Down Expand Up @@ -100,12 +101,7 @@ public static function assertCommandMatches(stdClass $expected, stdClass $actual
* preferable to skipping the txnNumber assertion. */
//unset($expected['txnNumber']);

foreach ($expected as $key => $value) {
if ($value === null) {
static::assertObjectNotHasAttribute($key, $actual);
unset($expected->{$key});
}
}
static::assertCommandOmittedFields($expected, $actual);

static::assertDocumentsMatch($expected, $actual);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"fields": [
{
"keyId": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedDate",
"bsonType": "date",
"queries": {
"queryType": "rangePreview",
"contention": {
"$numberLong": "0"
},
"sparsity": {
"$numberLong": "1"
},
"min": {
"$date": {
"$numberLong": "0"
}
},
"max": {
"$date": {
"$numberLong": "200"
}
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"fields": [
{
"keyId": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedDecimal",
"bsonType": "decimal",
"queries": {
"queryType": "rangePreview",
"contention": {
"$numberLong": "0"
},
"sparsity": {
"$numberLong": "1"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"fields": [
{
"keyId": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedDecimalPrecision",
"bsonType": "decimal",
"queries": {
"queryType": "rangePreview",
"contention": {
"$numberLong": "0"
},
"sparsity": {
"$numberLong": "1"
},
"min": {
"$numberDecimal": "0.0"
},
"max": {
"$numberDecimal": "200.0"
},
"precision": {
"$numberInt": "2"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"fields": [
{
"keyId": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedDouble",
"bsonType": "double",
"queries": {
"queryType": "rangePreview",
"contention": {
"$numberLong": "0"
},
"sparsity": {
"$numberLong": "1"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"fields": [
{
"keyId": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedDoublePrecision",
"bsonType": "double",
"queries": {
"queryType": "rangePreview",
"contention": {
"$numberLong": "0"
},
"sparsity": {
"$numberLong": "1"
},
"min": {
"$numberDouble": "0.0"
},
"max": {
"$numberDouble": "200.0"
},
"precision": {
"$numberInt": "2"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"fields": [
{
"keyId": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedInt",
"bsonType": "int",
"queries": {
"queryType": "rangePreview",
"contention": {
"$numberLong": "0"
},
"sparsity": {
"$numberLong": "1"
},
"min": {
"$numberInt": "0"
},
"max": {
"$numberInt": "200"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"fields": [
{
"keyId": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedLong",
"bsonType": "long",
"queries": {
"queryType": "rangePreview",
"contention": {
"$numberLong": "0"
},
"sparsity": {
"$numberLong": "1"
},
"min": {
"$numberLong": "0"
},
"max": {
"$numberLong": "200"
}
}
}
]
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
"escCollection": "enxcol_.default.esc",
"eccCollection": "enxcol_.default.ecc",
"ecocCollection": "enxcol_.default.ecoc",
"fields": [
{
"keyId": {
Expand Down Expand Up @@ -30,4 +27,4 @@
"bsonType": "string"
}
]
}
}
Loading