Skip to content

Commit 200d443

Browse files
committed
PHPLIB-851: Queryable encryption support for create/drop collection helpers
1 parent ee660a0 commit 200d443

9 files changed

+160
-5
lines changed

docs/includes/apiargs-MongoDBCollection-method-drop-option.yaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
source:
2-
file: apiargs-MongoDBCollection-common-option.yaml
3-
ref: typeMap
2+
file: apiargs-dropCollection-option.yaml
3+
ref: encryptedFields
44
post: |
5-
This will be used for the returned command result document.
5+
.. versionadded:: 1.13
66
---
77
source:
88
file: apiargs-common-option.yaml
99
ref: session
1010
post: |
1111
.. versionadded:: 1.3
1212
---
13+
source:
14+
file: apiargs-MongoDBCollection-common-option.yaml
15+
ref: typeMap
16+
post: |
17+
This will be used for the returned command result document.
18+
---
1319
source:
1420
file: apiargs-MongoDBCollection-common-option.yaml
1521
ref: writeConcern

docs/includes/apiargs-MongoDBDatabase-method-createCollection-option.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,24 @@ pre: |
6868
</reference/bson-type-comparison-order/#collation>` for the collection.
6969
---
7070
arg_name: option
71+
name: encryptedFields
72+
type: document
73+
description: |
74+
A document describing encrypted fields for queryable encryption. If omitted,
75+
the ``encryptedFieldsMap`` option within the ``autoEncryption`` driver option
76+
will be consulted. See the
77+
`Client Side Encryption specification <https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst>`_
78+
for more information.
79+
80+
This option is available in MongoDB 6.0+ and will result in an exception at
81+
execution time if specified for an older server version.
82+
83+
.. versionadded:: 1.13
84+
interface: phpmethod
85+
operation: ~
86+
optional: true
87+
---
88+
arg_name: option
7189
name: expireAfterSeconds
7290
type: integer
7391
description: |

docs/includes/apiargs-MongoDBDatabase-method-dropCollection-option.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
source:
2+
file: apiargs-dropCollection-option.yaml
3+
ref: encryptedFields
4+
post: |
5+
.. versionadded:: 1.13
6+
---
17
source:
28
file: apiargs-common-option.yaml
39
ref: session
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
arg_name: option
2+
name: encryptedFields
3+
type: array|object
4+
description: |
5+
A document describing encrypted fields for queryable encryption. If omitted,
6+
the ``encryptedFieldsMap`` option within the ``autoEncryption`` driver option
7+
will be consulted. If ``encryptedFieldsMap`` was defined but does not specify
8+
this collection, the library will make a final attempt to consult the
9+
server-side value for ``encryptedFields``. See the
10+
`Client Side Encryption specification <https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst>`_
11+
for more information.
12+
interface: phpmethod
13+
operation: ~
14+
optional: true
15+
...

src/Collection.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,21 @@ public function drop(array $options = [])
495495
$options['writeConcern'] = $this->writeConcern;
496496
}
497497

498+
$encryptedFields = $options['encryptedFields']
499+
?? get_encrypted_fields_from_driver($this->databaseName, $this->collectionName, $this->manager)
500+
?? get_encrypted_fields_from_server($this->databaseName, $this->collectionName, $this->manager, $server)
501+
?? null;
502+
503+
if ($encryptedFields !== null) {
504+
// encryptedFields is not passed to the drop command
505+
unset($options['encryptedFields']);
506+
507+
$encryptedFields = (array) $encryptedFields;
508+
(new DropCollection($this->databaseName, $encryptedFields['escCollection'] ?? 'enxcol_.' . $this->collectionName . '.esc'))->execute($server);
509+
(new DropCollection($this->databaseName, $encryptedFields['eccCollection'] ?? 'enxcol_.' . $this->collectionName . '.ecc'))->execute($server);
510+
(new DropCollection($this->databaseName, $encryptedFields['ecocCollection'] ?? 'enxcol_.' . $this->collectionName . '.ecoc'))->execute($server);
511+
}
512+
498513
$operation = new DropCollection($this->databaseName, $this->collectionName, $options);
499514

500515
return $operation->execute($server);

src/Database.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use MongoDB\Model\CollectionInfoIterator;
3434
use MongoDB\Operation\Aggregate;
3535
use MongoDB\Operation\CreateCollection;
36+
use MongoDB\Operation\CreateIndexes;
3637
use MongoDB\Operation\DatabaseCommand;
3738
use MongoDB\Operation\DropCollection;
3839
use MongoDB\Operation\DropDatabase;
@@ -275,9 +276,30 @@ public function createCollection($collectionName, array $options = [])
275276
$options['writeConcern'] = $this->writeConcern;
276277
}
277278

279+
$encryptedFields = $options['encryptedFields']
280+
?? get_encrypted_fields_from_driver($this->databaseName, $collectionName, $this->manager)
281+
?? null;
282+
283+
if ($encryptedFields !== null) {
284+
// encryptedFields is passed to the create command
285+
$options['encryptedFields'] = $encryptedFields;
286+
287+
$encryptedFields = (array) $encryptedFields;
288+
$enxcolOptions = ['clusteredIndex' => ['key' => ['_id' => 1], 'unique' => true]];
289+
(new CreateCollection($this->databaseName, $encryptedFields['escCollection'] ?? 'enxcol_.' . $collectionName . '.esc', $enxcolOptions))->execute($server);
290+
(new CreateCollection($this->databaseName, $encryptedFields['eccCollection'] ?? 'enxcol_.' . $collectionName . '.ecc', $enxcolOptions))->execute($server);
291+
(new CreateCollection($this->databaseName, $encryptedFields['ecocCollection'] ?? 'enxcol_.' . $collectionName . '.ecoc', $enxcolOptions))->execute($server);
292+
}
293+
278294
$operation = new CreateCollection($this->databaseName, $collectionName, $options);
279295

280-
return $operation->execute($server);
296+
$result = $operation->execute($server);
297+
298+
if ($encryptedFields !== null) {
299+
(new CreateIndexes($this->databaseName, $collectionName, [['key' => ['__safeContent__' => 1]]]))->execute($server);
300+
}
301+
302+
return $result;
281303
}
282304

283305
/**
@@ -330,6 +352,21 @@ public function dropCollection($collectionName, array $options = [])
330352
$options['writeConcern'] = $this->writeConcern;
331353
}
332354

355+
$encryptedFields = $options['encryptedFields']
356+
?? get_encrypted_fields_from_driver($this->databaseName, $collectionName, $this->manager)
357+
?? get_encrypted_fields_from_server($this->databaseName, $collectionName, $this->manager, $server)
358+
?? null;
359+
360+
if ($encryptedFields !== null) {
361+
// encryptedFields is not passed to the drop command
362+
unset($options['encryptedFields']);
363+
364+
$encryptedFields = (array) $encryptedFields;
365+
(new DropCollection($this->databaseName, $encryptedFields['escCollection'] ?? 'enxcol_.' . $collectionName . '.esc'))->execute($server);
366+
(new DropCollection($this->databaseName, $encryptedFields['eccCollection'] ?? 'enxcol_.' . $collectionName . '.ecc'))->execute($server);
367+
(new DropCollection($this->databaseName, $encryptedFields['ecocCollection'] ?? 'enxcol_.' . $collectionName . '.ecoc'))->execute($server);
368+
}
369+
333370
$operation = new DropCollection($this->databaseName, $collectionName, $options);
334371

335372
return $operation->execute($server);

src/Operation/CreateCollection.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class CreateCollection implements Executable
8383
*
8484
* * collation (document): Collation specification.
8585
*
86+
* * encryptedFields (document): CSFLE specification.
87+
*
8688
* * expireAfterSeconds: The TTL for documents in time series collections.
8789
*
8890
* This is not supported for servers versions < 5.0.
@@ -158,6 +160,10 @@ public function __construct($databaseName, $collectionName, array $options = [])
158160
throw InvalidArgumentException::invalidType('"collation" option', $options['collation'], 'array or object');
159161
}
160162

163+
if (isset($options['encryptedFields']) && ! is_array($options['encryptedFields']) && ! is_object($options['encryptedFields'])) {
164+
throw InvalidArgumentException::invalidType('"encryptedFields" option', $options['encryptedFields'], 'array or object');
165+
}
166+
161167
if (isset($options['expireAfterSeconds']) && ! is_integer($options['expireAfterSeconds'])) {
162168
throw InvalidArgumentException::invalidType('"expireAfterSeconds" option', $options['expireAfterSeconds'], 'integer');
163169
}
@@ -285,7 +291,7 @@ private function createCommand()
285291
}
286292
}
287293

288-
foreach (['changeStreamPreAndPostImages', 'clusteredIndex', 'collation', 'indexOptionDefaults', 'storageEngine', 'timeseries', 'validator'] as $option) {
294+
foreach (['changeStreamPreAndPostImages', 'clusteredIndex', 'collation', 'encryptedFields', 'indexOptionDefaults', 'storageEngine', 'timeseries', 'validator'] as $option) {
289295
if (isset($this->options[$option])) {
290296
$cmd[$option] = (object) $this->options[$option];
291297
}

src/functions.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use MongoDB\Driver\WriteConcern;
2828
use MongoDB\Exception\InvalidArgumentException;
2929
use MongoDB\Exception\RuntimeException;
30+
use MongoDB\Operation\ListCollections;
3031
use MongoDB\Operation\WithTransaction;
3132
use ReflectionClass;
3233
use ReflectionException;
@@ -123,6 +124,53 @@ function generate_index_name($document): string
123124
return $name;
124125
}
125126

127+
/**
128+
* Return a collection's encryptedFields from the encryptedFieldsMap
129+
* autoEncryption driver option (if available).
130+
*
131+
* @internal
132+
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#drop-collection-helper
133+
* @see Collection::drop
134+
* @see Database::createCollection
135+
* @see Database::dropCollection
136+
* @return array|object|null
137+
*/
138+
function get_encrypted_fields_from_driver(string $databaseName, string $collectionName, Manager $manager)
139+
{
140+
$encryptedFieldsMap = (array) $manager->getEncryptedFieldsMap();
141+
142+
return $encryptedFieldsMap[$databaseName . '.' . $collectionName] ?? null;
143+
}
144+
145+
/**
146+
* Return a collection's encryptedFields option from the server (if any).
147+
*
148+
* @internal
149+
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#drop-collection-helper
150+
* @see Collection::drop
151+
* @see Database::dropCollection
152+
* @return array|object|null
153+
*/
154+
function get_encrypted_fields_from_server(string $databaseName, string $collectionName, Manager $manager, Server $server)
155+
{
156+
// No-op if the encryptedFieldsMap autoEncryption driver option was omitted
157+
if ($manager->getEncryptedFieldsMap() === null) {
158+
return null;
159+
}
160+
161+
$collectionInfoIterator = (new ListCollections($databaseName, ['filter' => ['name' => $collectionName]]))->execute($server);
162+
163+
foreach ($collectionInfoIterator as $collectionInfo) {
164+
/* Note: ListCollections applies a typeMap that converts BSON documents
165+
* to PHP arrays. This should not be problematic as encryptedFields here
166+
* is only used by drop helpers to obtain names of supporting encryption
167+
* collections. */
168+
return $collectionInfo['options']['encryptedFields'] ?? null;
169+
}
170+
171+
return null;
172+
}
173+
126174
/**
127175
* Return whether the first key in the document starts with a "$" character.
128176
*

tests/Operation/CreateCollectionTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ public function provideInvalidConstructorOptions()
4747
$options[][] = ['collation' => $value];
4848
}
4949

50+
foreach ($this->getInvalidDocumentValues() as $value) {
51+
$options[][] = ['encryptedFields' => $value];
52+
}
53+
5054
foreach ($this->getInvalidIntegerValues() as $value) {
5155
$options[][] = ['expireAfterSeconds' => $value];
5256
}

0 commit comments

Comments
 (0)