-
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
Conversation
tests/SpecTests/ClientSideEncryption/Prose21_AutomaticDataEncryptionKeysTest.php
Show resolved
Hide resolved
tests/SpecTests/ClientSideEncryption/Prose21_AutomaticDataEncryptionKeysTest.php
Outdated
Show resolved
Hide resolved
tests/SpecTests/ClientSideEncryption/Prose21_AutomaticDataEncryptionKeysTest.php
Outdated
Show resolved
Hide resolved
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. |
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.
$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 comment
The reason will be displayed to describe this comment to others. Learn more.
I originally implemented two operation classes:
- CreateCollectionForQueryableEncryption for
createCollection()
with anencryptedFields
option, which composed several CreateCollection operations and one CreateIndexes operation. - CreateEncryptedCollection for this helper, which composed a CreateCollectionForQueryableEncryption.
In that approach, CreateEncryptedCollection::execute()
created data keys before calling CreateCollectionForQueryableEncryption::execute()
and contained the logic for CreateEncryptedCollectionException. The downside was updating the options for the inner-most CreateCollection operation when encryptedFields
changed.
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 createDataKeys()
method to be called prior to execute()
. This made it easier to reconstruct the inner CreateCollection operation.
Using a pass-by-reference parameter to expose modifications to encryptedFields
allowed me to avoid dealing with CreateEncryptedCollectionException in the operation class entirely. I briefly explored returning encryptedFields
and throwing the exception when needed, and it made both createDataKeys()
and the helper's try/catch
more complicated.
$result = $operation->execute($server); | ||
|
||
return [$result, $encryptedFields]; | ||
} catch (Throwable $e) { |
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 considered limiting this to \Exception
, but that would have required silencing a phpcs warning. I think it's unlikely we'd ever get an internal PHP error here, but there's no harm in wrapping it.
This is unlike UnifiedSpecTest.php, which has a specific reason to avoid catching errors.
|
||
return [$result, $encryptedFields]; | ||
} catch (Throwable $e) { | ||
throw new CreateEncryptedCollectionException($e, $encryptedFields ?? []); |
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.
$encryptedFields ?? []
satisfies two psalm warnings:
- PossiblyUndefinedVariable: Possibly undefined variable $encryptedFields defined in try block (see https://psalm.dev/018)
- PossiblyNullArgument: Argument 2 of CreateEncryptedCollectionException::__construct cannot be null, possibly null value provided (see https://psalm.dev/078)
In practice, I don't think that's possible since it is assigned in the first line of createDataKeys()
and we've already verified that the option exists in the operation constructor.
I'm open to any feedback, but this seemed like a harmless, defensive fix.
namespace MongoDB\Operation; | ||
|
||
// phpcs:disable SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse | ||
use MongoDB\BSON\Binary; |
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.
phpcs does not parse @psalm-var
annotations, and using @var
instead would produce a difference error:
Invalid inline documentation comment format
@var array{fields: list<array{keyId: ?Binary}|object{keyId: ?Binary}>}
, expected@var type $variable
private $options; | ||
|
||
/** | ||
* @see CreateCollection::__construct() for supported options |
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 can't tell you how great it felt to write an operation constructor doc block with option documentation copypasta.
continue; | ||
} | ||
|
||
$field = (array) $field; |
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.
The earliest version of this logic relied on recursive_copy()
to ensure we didn't inadvertently modify an object still referenced by the caller. I eventually realized that array casting here would allow me to avoid that risk and eliminate an extra code path I had for $field
being an object.
} | ||
} | ||
|
||
$this->options['encryptedFields'] = $encryptedFields; |
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.
Technically, this is only necessary if one or more data keys were created in the preceding foreach
; however, the optimization didn't seem necessary (this is hardly a hot code path).
/** @var ClientEncryption */ | ||
private $clientEncryption; | ||
|
||
public function setUp(): void |
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.
Most of this is lifted from the CSFLE prose tests. Copying this over reminded me of our previous discussion about using traits (vs. piling more onto the base FunctionalTestCase class), but that's something for a future PR.
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.
LGTM - feel free to apply or ignore the suggested patches at your own discretion.
I like the way CreateEncryptedCollection
is structured, and that people don't necessarily need to call $db->createEncryptedCollection
to create an encrypted collection but can rely on createCollection
to do the right thing. 🎉
Co-authored-by: Andreas Braun <[email protected]>
Coding standard CI failure is due to PHPLIB-1106 and will be addressed by #1058. The CI build failures for |
https://jira.mongodb.org/browse/PHPLIB-913