Skip to content

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

Merged
merged 21 commits into from
Apr 12, 2023

Conversation

jmikola
Copy link
Member

@jmikola jmikola commented Mar 20, 2023

@jmikola jmikola requested a review from alcaeus March 21, 2023 04:56
@jmikola jmikola marked this pull request as ready for review March 22, 2023 09:28
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.
Copy link
Member Author

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);
Copy link
Member Author

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 an encryptedFields 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) {
Copy link
Member Author

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 ?? []);
Copy link
Member Author

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;
Copy link
Member Author

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
Copy link
Member Author

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;
Copy link
Member Author

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;
Copy link
Member Author

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
Copy link
Member Author

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.

Copy link
Member

@alcaeus alcaeus left a 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. 🎉

@jmikola
Copy link
Member Author

jmikola commented Apr 12, 2023

Coding standard CI failure is due to PHPLIB-1106 and will be addressed by #1058.

The CI build failures for mongodb-latest are due to PHPLIB-1088 and will be addressed in a subsequent PR.

@jmikola jmikola merged commit b963821 into mongodb:master Apr 12, 2023
@jmikola jmikola deleted the phplib-913 branch April 12, 2023 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants