Skip to content

Commit b963821

Browse files
jmikolaalcaeus
andauthored
PHPLIB-913: Database::createEncryptedCollection() helper (#1050)
* Docs for Database::createEncryptedCollection() * Revise docs and require $options since encryptedFields is required * Add example for creating new Client with auto encryption * Permit coding standard deviations in CSFLE prose tests * Extract Database methods to CreateEncryptedCollection operation * Functional tests for CreateEncryptedCollection::createDataKeys() * Test that createDataKey() doesn't modify encryptedFields object option * Require replica sets for CreateEncryptedCollectionFunctionalTest * Add reference links to CreateCollection operation docs Co-authored-by: Andreas Braun <[email protected]>
1 parent 4067844 commit b963821

16 files changed

+1042
-30
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
source:
2+
file: apiargs-common-param.yaml
3+
ref: $collectionName
4+
replacement:
5+
subject: "encrypted collection"
6+
action: " to create"
7+
---
8+
arg_name: param
9+
name: $clientEncryption
10+
type: :php:`MongoDB\\Driver\\ClientEncryption <mongodb-driver-clientencryption>`
11+
description: |
12+
The ClientEncryption object used to create data keys.
13+
interface: phpmethod
14+
operation: ~
15+
optional: false
16+
---
17+
arg_name: param
18+
name: $kmsProvider
19+
type: string
20+
description: |
21+
KMS provider (e.g. "local", "aws") that will be used to encrypt new data keys.
22+
This corresponds to the ``$kmsProvider`` parameter for
23+
:php:`MongoDB\\Driver\\ClientEncryption::createDataKey() <mongodb-driver-clientencryption.createdatakey>`.
24+
interface: phpmethod
25+
operation: ~
26+
optional: false
27+
---
28+
arg_name: param
29+
name: $masterKey
30+
type: array|null
31+
description: |
32+
KMS-specific key options that will be used to encrypt new data keys. This
33+
corresponds to the ``masterKey`` option for
34+
:php:`MongoDB\\Driver\\ClientEncryption::createDataKey() <mongodb-driver-clientencryption.createdatakey>`.
35+
36+
If ``$kmsProvider`` is "local", this should be ``null``.
37+
interface: phpmethod
38+
operation: ~
39+
optional: false
40+
---
41+
source:
42+
file: apiargs-common-param.yaml
43+
ref: $options
44+
optional: false
45+
post: |
46+
The ``encryptedFields`` option is required.
47+
...

docs/reference/class/MongoDBDatabase.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Methods
4848
/reference/method/MongoDBDatabase-aggregate
4949
/reference/method/MongoDBDatabase-command
5050
/reference/method/MongoDBDatabase-createCollection
51+
/reference/method/MongoDBDatabase-createEncryptedCollection
5152
/reference/method/MongoDBDatabase-drop
5253
/reference/method/MongoDBDatabase-dropCollection
5354
/reference/method/MongoDBDatabase-getDatabaseName

docs/reference/exception-classes.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ MongoDB\\Exception\\BadMethodCallException
3030

3131
----
3232

33+
MongoDB\\Exception\\CreateEncryptedCollectionException
34+
------------------------------------------------------
35+
36+
.. phpclass:: MongoDB\\Exception\\CreateEncryptedCollectionException
37+
38+
Thrown by :phpmethod:`MongoDB\\Database::createEncryptedCollection()` if any
39+
error is encountered while creating data keys or creating the collection. The
40+
original exception and modified ``encryptedFields`` option can be accessed
41+
via the ``getPrevious()`` and ``getEncryptedFields()`` methods, respectively.
42+
43+
This class extends the library's :phpclass:`RuntimeException
44+
<MongoDB\\Exception\\RuntimeException>` class.
45+
46+
----
47+
3348
MongoDB\\Exception\\InvalidArgumentException
3449
--------------------------------------------
3550

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
==============================================
2+
MongoDB\\Database::createEncryptedCollection()
3+
==============================================
4+
5+
.. versionadded:: 1.16
6+
7+
.. default-domain:: mongodb
8+
9+
.. contents:: On this page
10+
:local:
11+
:backlinks: none
12+
:depth: 1
13+
:class: singlecol
14+
15+
.. note::
16+
17+
Queryable Encryption is in public preview and available for evaluation
18+
purposes. It is not yet recommended for production deployments as breaking
19+
changes may be introduced. See the
20+
`Queryable Encryption Preview <https://www.mongodb.com/blog/post/mongodb-releases-queryable-encryption-preview/>`_
21+
blog post for more information.
22+
23+
Definition
24+
----------
25+
26+
.. phpmethod:: MongoDB\\Database::createEncryptedCollection()
27+
28+
Explicitly creates an encrypted collection.
29+
30+
.. code-block:: php
31+
32+
function createEncryptedCollection(string $collectionName, MongoDB\Driver\ClientEncryption $clientEncryption, string $kmsProvider, ?array $masterKey, array $options): array
33+
34+
This method will automatically create data keys for any encrypted fields
35+
where ``keyId`` is ``null``. Data keys will be created using
36+
:php:`MongoDB\\Driver\\ClientEncryption::createDataKey() <mongodb-driver-clientencryption.createdatakey>`
37+
and the provided ``$kmsProvider`` and ``$masterKey`` parameters. A copy of
38+
the modified ``encryptedFields`` option will be returned in addition to the
39+
result from creating the collection.
40+
41+
This method does not affect any auto encryption settings on existing
42+
:phpclass:`MongoDB\\Client` objects. Users must configure auto encryption
43+
after creating the encrypted collection with ``createEncryptedCollection()``.
44+
45+
This method has the following parameters:
46+
47+
.. include:: /includes/apiargs/MongoDBDatabase-method-createEncryptedCollection-param.rst
48+
49+
The ``$options`` parameter supports the same options as
50+
:phpmethod:`MongoDB\\Database::createCollection()`. The ``encryptedFields``
51+
option is required.
52+
53+
Return Values
54+
-------------
55+
56+
A tuple (i.e. two-element array) containing the result document from the
57+
:manual:`create </reference/command/create>` command (an array or object
58+
according to the ``typeMap`` option) and the modified ``encryptedFields``
59+
option.
60+
61+
Errors/Exceptions
62+
-----------------
63+
64+
:phpclass:`MongoDB\\Exception\\CreateEncryptedCollectionException` if any error
65+
is encountered creating data keys or the collection. The original exception and
66+
modified ``encryptedFields`` option can be accessed via the ``getPrevious()``
67+
and ``getEncryptedFields()`` methods, respectively.
68+
69+
.. include:: /includes/extracts/error-invalidargumentexception.rst
70+
71+
Example
72+
-------
73+
74+
The following example creates an encrypted ``users`` collection in the ``test``
75+
database. The ``ssn`` field within the ``users`` collection will be defined as
76+
an encrypted string field.
77+
78+
.. code-block:: php
79+
80+
<?php
81+
82+
// 96-byte master key used to encrypt/decrypt data keys
83+
define('LOCAL_MASTERKEY', '...');
84+
85+
$client = new MongoDB\Client;
86+
87+
$clientEncryption = $client->createClientEncryption([
88+
'keyVaultNamespace' => 'keyvault.datakeys',
89+
'kmsProviders' => [
90+
'local' => ['key' => new MongoDB\BSON\Binary(base64_decode(LOCAL_MASTERKEY), 0)],
91+
],
92+
);
93+
94+
[$result, $encryptedFields] = $client->test->createEncryptedCollection(
95+
'users',
96+
$clientEncryption,
97+
'local',
98+
null,
99+
[
100+
'encryptedFields' => [
101+
'fields' => [
102+
['path' => 'ssn', 'bsonType' => 'string', 'keyId' => null],
103+
],
104+
],
105+
]
106+
);
107+
108+
If the encrypted collection was successfully created, ``$result`` will contain
109+
the response document from the ``create`` command and
110+
``$encryptedFields['fields'][0]['keyId']`` will contain a
111+
:php:`MongoDB\\BSON\\Binary <class.mongodb-bson-binary>` object with subtype 4
112+
(i.e. UUID).
113+
114+
The modified ``encryptedFields`` option can then be used to construct a new
115+
:phpclass:`MongoDB\\Client` with auto encryption enabled.
116+
117+
.. code-block:: php
118+
119+
<?php
120+
121+
$encryptedClient = new MongoDB\Client(
122+
null, // Connection string
123+
[], // Additional connection string options
124+
[
125+
'autoEncryption' => [
126+
'keyVaultNamespace' => 'keyvault.datakeys',
127+
'kmsProviders' => [
128+
'local' => ['key' => new MongoDB\BSON\Binary(base64_decode(LOCAL_MASTERKEY), 0)],
129+
],
130+
'encryptedFieldsMap' => [
131+
'test.users' => $encryptedFields,
132+
],
133+
],
134+
]
135+
);
136+
137+
See Also
138+
--------
139+
140+
- :phpmethod:`MongoDB\\Database::createCollection()`
141+
- :phpmethod:`MongoDB\\Client::createClientEncryption()`
142+
- :php:`MongoDB\\Driver\\ClientEncryption::createDataKey() <mongodb-driver-clientencryption.createdatakey>`
143+
- :manual:`create </reference/command/create>` command reference in the MongoDB
144+
manual

phpcs.xml.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
<exclude-pattern>/src/GridFS/StreamWrapper</exclude-pattern>
176176
<exclude-pattern>/tests/DocumentationExamplesTest.php</exclude-pattern>
177177
<exclude-pattern>/tests/GridFS/UnusableStream.php</exclude-pattern>
178+
<exclude-pattern>/tests/SpecTests/ClientSideEncryption/Prose*</exclude-pattern>
178179
</rule>
179180
<rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses">
180181
<exclude-pattern>/examples</exclude-pattern>
@@ -183,4 +184,7 @@
183184
<rule ref="Squiz.Classes.ClassFileName.NoMatch">
184185
<exclude-pattern>/examples</exclude-pattern>
185186
</rule>
187+
<rule ref="Squiz.Classes.ValidClassName.NotCamelCaps">
188+
<exclude-pattern>/tests/SpecTests/ClientSideEncryption/Prose*</exclude-pattern>
189+
</rule>
186190
</ruleset>

psalm-baseline.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,12 @@
491491
<code>$options['writeConcern']</code>
492492
</MixedAssignment>
493493
</file>
494+
<file src="src/Operation/CreateEncryptedCollection.php">
495+
<DocblockTypeContradiction occurrences="2">
496+
<code>! is_array($encryptedFields['fields'])</code>
497+
<code>! is_array($field) &amp;&amp; ! is_object($field)</code>
498+
</DocblockTypeContradiction>
499+
</file>
494500
<file src="src/Operation/CreateIndexes.php">
495501
<DocblockTypeContradiction occurrences="1">
496502
<code>is_array($index)</code>

src/Database.php

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
namespace MongoDB;
1919

2020
use Iterator;
21+
use MongoDB\Driver\ClientEncryption;
2122
use MongoDB\Driver\Cursor;
2223
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
2324
use MongoDB\Driver\Manager;
2425
use MongoDB\Driver\ReadConcern;
2526
use MongoDB\Driver\ReadPreference;
2627
use MongoDB\Driver\WriteConcern;
28+
use MongoDB\Exception\CreateEncryptedCollectionException;
2729
use MongoDB\Exception\InvalidArgumentException;
2830
use MongoDB\Exception\UnexpectedValueException;
2931
use MongoDB\Exception\UnsupportedException;
@@ -33,7 +35,7 @@
3335
use MongoDB\Model\CollectionInfoIterator;
3436
use MongoDB\Operation\Aggregate;
3537
use MongoDB\Operation\CreateCollection;
36-
use MongoDB\Operation\CreateIndexes;
38+
use MongoDB\Operation\CreateEncryptedCollection;
3739
use MongoDB\Operation\DatabaseCommand;
3840
use MongoDB\Operation\DropCollection;
3941
use MongoDB\Operation\DropDatabase;
@@ -42,6 +44,7 @@
4244
use MongoDB\Operation\ModifyCollection;
4345
use MongoDB\Operation\RenameCollection;
4446
use MongoDB\Operation\Watch;
47+
use Throwable;
4548
use Traversable;
4649

4750
use function is_array;
@@ -256,7 +259,13 @@ public function command($command, array $options = [])
256259
/**
257260
* Create a new collection explicitly.
258261
*
262+
* If the "encryptedFields" option is specified, this method additionally
263+
* creates related metadata collections and an index on the encrypted
264+
* collection.
265+
*
259266
* @see CreateCollection::__construct() for supported options
267+
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#create-collection-helper
268+
* @see https://www.mongodb.com/docs/manual/core/queryable-encryption/fundamentals/manage-collections/
260269
* @return array|object Command result document
261270
* @throws UnsupportedException if options are not supported by the selected server
262271
* @throws InvalidArgumentException for parameter/option parsing errors
@@ -268,36 +277,63 @@ public function createCollection(string $collectionName, array $options = [])
268277
$options['typeMap'] = $this->typeMap;
269278
}
270279

271-
$server = select_server($this->manager, $options);
272-
273280
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
274281
$options['writeConcern'] = $this->writeConcern;
275282
}
276283

277-
$encryptedFields = $options['encryptedFields']
278-
?? get_encrypted_fields_from_driver($this->databaseName, $collectionName, $this->manager)
279-
?? null;
284+
if (! isset($options['encryptedFields'])) {
285+
$options['encryptedFields'] = get_encrypted_fields_from_driver($this->databaseName, $collectionName, $this->manager);
286+
}
280287

281-
if ($encryptedFields !== null) {
282-
// encryptedFields is passed to the create command
283-
$options['encryptedFields'] = $encryptedFields;
288+
$operation = isset($options['encryptedFields'])
289+
? new CreateEncryptedCollection($this->databaseName, $collectionName, $options)
290+
: new CreateCollection($this->databaseName, $collectionName, $options);
284291

285-
$encryptedFields = (array) $encryptedFields;
286-
$enxcolOptions = ['clusteredIndex' => ['key' => ['_id' => 1], 'unique' => true]];
287-
(new CreateCollection($this->databaseName, $encryptedFields['escCollection'] ?? 'enxcol_.' . $collectionName . '.esc', $enxcolOptions))->execute($server);
288-
(new CreateCollection($this->databaseName, $encryptedFields['eccCollection'] ?? 'enxcol_.' . $collectionName . '.ecc', $enxcolOptions))->execute($server);
289-
(new CreateCollection($this->databaseName, $encryptedFields['ecocCollection'] ?? 'enxcol_.' . $collectionName . '.ecoc', $enxcolOptions))->execute($server);
290-
}
292+
$server = select_server($this->manager, $options);
291293

292-
$operation = new CreateCollection($this->databaseName, $collectionName, $options);
294+
return $operation->execute($server);
295+
}
293296

294-
$result = $operation->execute($server);
297+
/**
298+
* Create a new encrypted collection explicitly.
299+
*
300+
* The "encryptedFields" option is required.
301+
*
302+
* This method will automatically create data keys for any encrypted fields
303+
* where "keyId" is null. A copy of the modified "encryptedFields" option
304+
* will be returned in addition to the result from creating the collection.
305+
*
306+
* If any error is encountered creating data keys or the collection, a
307+
* CreateEncryptedCollectionException will be thrown. The original exception
308+
* and modified "encryptedFields" option can be accessed via the
309+
* getPrevious() and getEncryptedFields() methods, respectively.
310+
*
311+
* @see CreateCollection::__construct() for supported options
312+
* @return array A tuple containing the command result document from creating the collection and the modified "encryptedFields" option
313+
* @throws InvalidArgumentException for parameter/option parsing errors
314+
* @throws CreateEncryptedCollectionException for any errors creating data keys or creating the collection
315+
*/
316+
public function createEncryptedCollection(string $collectionName, ClientEncryption $clientEncryption, string $kmsProvider, ?array $masterKey, array $options): array
317+
{
318+
if (! isset($options['typeMap'])) {
319+
$options['typeMap'] = $this->typeMap;
320+
}
295321

296-
if ($encryptedFields !== null) {
297-
(new CreateIndexes($this->databaseName, $collectionName, [['key' => ['__safeContent__' => 1]]]))->execute($server);
322+
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) {
323+
$options['writeConcern'] = $this->writeConcern;
298324
}
299325

300-
return $result;
326+
$operation = new CreateEncryptedCollection($this->databaseName, $collectionName, $options);
327+
$server = select_server($this->manager, $options);
328+
329+
try {
330+
$operation->createDataKeys($clientEncryption, $kmsProvider, $masterKey, $encryptedFields);
331+
$result = $operation->execute($server);
332+
333+
return [$result, $encryptedFields];
334+
} catch (Throwable $e) {
335+
throw new CreateEncryptedCollectionException($e, $encryptedFields ?? []);
336+
}
301337
}
302338

303339
/**

0 commit comments

Comments
 (0)