-
Notifications
You must be signed in to change notification settings - Fork 266
PHPLIB-913: Database::createEncryptedCollection() helper #1050
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
Changes from all commits
6d76fba
14d6a26
89d03f4
e78f478
c02d343
390299f
5107633
0ba8a28
d244f07
27dca00
8d4eeb1
521855d
e5d736e
801784d
f334816
8dd84f4
7c4c13c
28b86e1
645fdee
8f088e2
7c7b0a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
source: | ||
file: apiargs-common-param.yaml | ||
ref: $collectionName | ||
replacement: | ||
subject: "encrypted collection" | ||
action: " to create" | ||
--- | ||
arg_name: param | ||
name: $clientEncryption | ||
type: :php:`MongoDB\\Driver\\ClientEncryption <mongodb-driver-clientencryption>` | ||
description: | | ||
The ClientEncryption object used to create data keys. | ||
interface: phpmethod | ||
operation: ~ | ||
optional: false | ||
--- | ||
arg_name: param | ||
name: $kmsProvider | ||
type: string | ||
description: | | ||
KMS provider (e.g. "local", "aws") that will be used to encrypt new data keys. | ||
This corresponds to the ``$kmsProvider`` parameter for | ||
:php:`MongoDB\\Driver\\ClientEncryption::createDataKey() <mongodb-driver-clientencryption.createdatakey>`. | ||
interface: phpmethod | ||
operation: ~ | ||
optional: false | ||
--- | ||
arg_name: param | ||
name: $masterKey | ||
type: array|null | ||
description: | | ||
KMS-specific key options that will be used to encrypt new data keys. This | ||
corresponds to the ``masterKey`` option for | ||
:php:`MongoDB\\Driver\\ClientEncryption::createDataKey() <mongodb-driver-clientencryption.createdatakey>`. | ||
|
||
If ``$kmsProvider`` is "local", this should be ``null``. | ||
interface: phpmethod | ||
operation: ~ | ||
optional: false | ||
--- | ||
source: | ||
file: apiargs-common-param.yaml | ||
ref: $options | ||
optional: false | ||
post: | | ||
The ``encryptedFields`` option is required. | ||
... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
============================================== | ||
MongoDB\\Database::createEncryptedCollection() | ||
============================================== | ||
|
||
.. versionadded:: 1.16 | ||
|
||
.. default-domain:: mongodb | ||
|
||
.. contents:: On this page | ||
:local: | ||
:backlinks: none | ||
:depth: 1 | ||
:class: singlecol | ||
|
||
.. note:: | ||
|
||
Queryable Encryption is in public preview and available for evaluation | ||
purposes. It is not yet recommended for production deployments as breaking | ||
changes may be introduced. See the | ||
`Queryable Encryption Preview <https://www.mongodb.com/blog/post/mongodb-releases-queryable-encryption-preview/>`_ | ||
blog post for more information. | ||
|
||
Definition | ||
---------- | ||
|
||
.. phpmethod:: MongoDB\\Database::createEncryptedCollection() | ||
|
||
Explicitly creates an encrypted collection. | ||
|
||
.. code-block:: php | ||
|
||
function createEncryptedCollection(string $collectionName, MongoDB\Driver\ClientEncryption $clientEncryption, string $kmsProvider, ?array $masterKey, array $options): array | ||
|
||
This method will automatically create data keys for any encrypted fields | ||
where ``keyId`` is ``null``. Data keys will be created using | ||
:php:`MongoDB\\Driver\\ClientEncryption::createDataKey() <mongodb-driver-clientencryption.createdatakey>` | ||
and the provided ``$kmsProvider`` and ``$masterKey`` parameters. A copy of | ||
the modified ``encryptedFields`` option will be returned in addition to the | ||
result from creating the collection. | ||
|
||
This method does not affect any auto encryption settings on existing | ||
:phpclass:`MongoDB\\Client` objects. Users must configure auto encryption | ||
after creating the encrypted collection with ``createEncryptedCollection()``. | ||
|
||
This method has the following parameters: | ||
|
||
.. include:: /includes/apiargs/MongoDBDatabase-method-createEncryptedCollection-param.rst | ||
|
||
The ``$options`` parameter supports the same options as | ||
:phpmethod:`MongoDB\\Database::createCollection()`. The ``encryptedFields`` | ||
option is required. | ||
|
||
Return Values | ||
------------- | ||
|
||
A tuple (i.e. two-element array) containing the result document from the | ||
:manual:`create </reference/command/create>` command (an array or object | ||
according to the ``typeMap`` option) and the modified ``encryptedFields`` | ||
option. | ||
|
||
Errors/Exceptions | ||
----------------- | ||
|
||
:phpclass:`MongoDB\\Exception\\CreateEncryptedCollectionException` if any error | ||
is encountered creating data keys or the collection. The original exception and | ||
modified ``encryptedFields`` option can be accessed via the ``getPrevious()`` | ||
and ``getEncryptedFields()`` methods, respectively. | ||
|
||
.. include:: /includes/extracts/error-invalidargumentexception.rst | ||
|
||
Example | ||
------- | ||
|
||
The following example creates an encrypted ``users`` collection in the ``test`` | ||
database. The ``ssn`` field within the ``users`` collection will be defined as | ||
an encrypted string field. | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
// 96-byte master key used to encrypt/decrypt data keys | ||
define('LOCAL_MASTERKEY', '...'); | ||
|
||
$client = new MongoDB\Client; | ||
|
||
$clientEncryption = $client->createClientEncryption([ | ||
'keyVaultNamespace' => 'keyvault.datakeys', | ||
'kmsProviders' => [ | ||
'local' => ['key' => new MongoDB\BSON\Binary(base64_decode(LOCAL_MASTERKEY), 0)], | ||
], | ||
); | ||
|
||
[$result, $encryptedFields] = $client->test->createEncryptedCollection( | ||
'users', | ||
$clientEncryption, | ||
'local', | ||
null, | ||
[ | ||
'encryptedFields' => [ | ||
'fields' => [ | ||
['path' => 'ssn', 'bsonType' => 'string', 'keyId' => null], | ||
], | ||
], | ||
] | ||
); | ||
|
||
If the encrypted collection was successfully created, ``$result`` will contain | ||
the response document from the ``create`` command and | ||
``$encryptedFields['fields'][0]['keyId']`` will contain a | ||
:php:`MongoDB\\BSON\\Binary <class.mongodb-bson-binary>` object with subtype 4 | ||
(i.e. UUID). | ||
|
||
The modified ``encryptedFields`` option can then be used to construct a new | ||
:phpclass:`MongoDB\\Client` with auto encryption enabled. | ||
|
||
.. code-block:: php | ||
|
||
<?php | ||
|
||
$encryptedClient = new MongoDB\Client( | ||
null, // Connection string | ||
[], // Additional connection string options | ||
[ | ||
'autoEncryption' => [ | ||
'keyVaultNamespace' => 'keyvault.datakeys', | ||
'kmsProviders' => [ | ||
'local' => ['key' => new MongoDB\BSON\Binary(base64_decode(LOCAL_MASTERKEY), 0)], | ||
], | ||
'encryptedFieldsMap' => [ | ||
'test.users' => $encryptedFields, | ||
], | ||
], | ||
] | ||
); | ||
|
||
See Also | ||
-------- | ||
|
||
- :phpmethod:`MongoDB\\Database::createCollection()` | ||
- :phpmethod:`MongoDB\\Client::createClientEncryption()` | ||
- :php:`MongoDB\\Driver\\ClientEncryption::createDataKey() <mongodb-driver-clientencryption.createdatakey>` | ||
- :manual:`create </reference/command/create>` command reference in the MongoDB | ||
manual |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,12 +18,14 @@ | |
namespace MongoDB; | ||
|
||
use Iterator; | ||
use MongoDB\Driver\ClientEncryption; | ||
use MongoDB\Driver\Cursor; | ||
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; | ||
use MongoDB\Driver\Manager; | ||
use MongoDB\Driver\ReadConcern; | ||
use MongoDB\Driver\ReadPreference; | ||
use MongoDB\Driver\WriteConcern; | ||
use MongoDB\Exception\CreateEncryptedCollectionException; | ||
use MongoDB\Exception\InvalidArgumentException; | ||
use MongoDB\Exception\UnexpectedValueException; | ||
use MongoDB\Exception\UnsupportedException; | ||
|
@@ -33,7 +35,7 @@ | |
use MongoDB\Model\CollectionInfoIterator; | ||
use MongoDB\Operation\Aggregate; | ||
use MongoDB\Operation\CreateCollection; | ||
use MongoDB\Operation\CreateIndexes; | ||
use MongoDB\Operation\CreateEncryptedCollection; | ||
use MongoDB\Operation\DatabaseCommand; | ||
use MongoDB\Operation\DropCollection; | ||
use MongoDB\Operation\DropDatabase; | ||
|
@@ -42,6 +44,7 @@ | |
use MongoDB\Operation\ModifyCollection; | ||
use MongoDB\Operation\RenameCollection; | ||
use MongoDB\Operation\Watch; | ||
use Throwable; | ||
use Traversable; | ||
|
||
use function is_array; | ||
|
@@ -256,7 +259,13 @@ public function command($command, array $options = []) | |
/** | ||
* Create a new collection explicitly. | ||
* | ||
* If the "encryptedFields" option is specified, this method additionally | ||
* creates related metadata collections and an index on the encrypted | ||
* collection. | ||
* | ||
* @see CreateCollection::__construct() for supported options | ||
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/client-side-encryption.rst#create-collection-helper | ||
* @see https://www.mongodb.com/docs/manual/core/queryable-encryption/fundamentals/manage-collections/ | ||
* @return array|object Command result document | ||
* @throws UnsupportedException if options are not supported by the selected server | ||
* @throws InvalidArgumentException for parameter/option parsing errors | ||
|
@@ -268,36 +277,63 @@ public function createCollection(string $collectionName, array $options = []) | |
$options['typeMap'] = $this->typeMap; | ||
} | ||
|
||
$server = select_server($this->manager, $options); | ||
|
||
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) { | ||
$options['writeConcern'] = $this->writeConcern; | ||
} | ||
|
||
$encryptedFields = $options['encryptedFields'] | ||
?? get_encrypted_fields_from_driver($this->databaseName, $collectionName, $this->manager) | ||
?? null; | ||
if (! isset($options['encryptedFields'])) { | ||
$options['encryptedFields'] = get_encrypted_fields_from_driver($this->databaseName, $collectionName, $this->manager); | ||
} | ||
|
||
if ($encryptedFields !== null) { | ||
// encryptedFields is passed to the create command | ||
$options['encryptedFields'] = $encryptedFields; | ||
$operation = isset($options['encryptedFields']) | ||
? new CreateEncryptedCollection($this->databaseName, $collectionName, $options) | ||
: new CreateCollection($this->databaseName, $collectionName, $options); | ||
|
||
$encryptedFields = (array) $encryptedFields; | ||
$enxcolOptions = ['clusteredIndex' => ['key' => ['_id' => 1], 'unique' => true]]; | ||
(new CreateCollection($this->databaseName, $encryptedFields['escCollection'] ?? 'enxcol_.' . $collectionName . '.esc', $enxcolOptions))->execute($server); | ||
(new CreateCollection($this->databaseName, $encryptedFields['eccCollection'] ?? 'enxcol_.' . $collectionName . '.ecc', $enxcolOptions))->execute($server); | ||
(new CreateCollection($this->databaseName, $encryptedFields['ecocCollection'] ?? 'enxcol_.' . $collectionName . '.ecoc', $enxcolOptions))->execute($server); | ||
} | ||
$server = select_server($this->manager, $options); | ||
|
||
$operation = new CreateCollection($this->databaseName, $collectionName, $options); | ||
return $operation->execute($server); | ||
} | ||
|
||
$result = $operation->execute($server); | ||
/** | ||
* Create a new encrypted collection explicitly. | ||
* | ||
* The "encryptedFields" option is required. | ||
* | ||
* This method will automatically create data keys for any encrypted fields | ||
* where "keyId" is null. A copy of the modified "encryptedFields" option | ||
* will be returned in addition to the result from creating the collection. | ||
* | ||
* If any error is encountered creating data keys or the collection, a | ||
* CreateEncryptedCollectionException will be thrown. The original exception | ||
* and modified "encryptedFields" option can be accessed via the | ||
* getPrevious() and getEncryptedFields() methods, respectively. | ||
* | ||
* @see CreateCollection::__construct() for supported 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 | ||
*/ | ||
public function createEncryptedCollection(string $collectionName, ClientEncryption $clientEncryption, string $kmsProvider, ?array $masterKey, array $options): array | ||
{ | ||
if (! isset($options['typeMap'])) { | ||
$options['typeMap'] = $this->typeMap; | ||
} | ||
|
||
if ($encryptedFields !== null) { | ||
(new CreateIndexes($this->databaseName, $collectionName, [['key' => ['__safeContent__' => 1]]]))->execute($server); | ||
if (! isset($options['writeConcern']) && ! is_in_transaction($options)) { | ||
$options['writeConcern'] = $this->writeConcern; | ||
} | ||
|
||
return $result; | ||
$operation = new CreateEncryptedCollection($this->databaseName, $collectionName, $options); | ||
$server = select_server($this->manager, $options); | ||
|
||
try { | ||
$operation->createDataKeys($clientEncryption, $kmsProvider, $masterKey, $encryptedFields); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally implemented two operation classes:
In that approach, I also wasn't too happy with the naming for both operations. It made sense for CreateEncryptedCollection to match the helper method, but it was only responsible for creating data keys. CreateCollectionForQueryableEncryption was actually creating an encrypted collection. I then combined both operations into CreateEncryptedCollection and moved the key generation logic to a separate Using a pass-by-reference parameter to expose modifications to |
||
$result = $operation->execute($server); | ||
|
||
return [$result, $encryptedFields]; | ||
alcaeus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} catch (Throwable $e) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered limiting this to This is unlike UnifiedSpecTest.php, which has a specific reason to avoid catching errors. |
||
throw new CreateEncryptedCollectionException($e, $encryptedFields ?? []); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In practice, I don't think that's possible since it is assigned in the first line of I'm open to any feedback, but this seemed like a harmless, defensive fix. |
||
} | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized I never added this note to the previous Queryable Encryption APIs we introduced in 1.13. I opened PHPLIB-1095 and #1053 to address that.