Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 85 additions & 30 deletions lib/Doctrine/ODM/MongoDB/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use InvalidArgumentException;
use Jean85\PrettyVersions;
use LogicException;
use MongoDB\Client;
use MongoDB\Driver\Manager;
use MongoDB\Driver\WriteConcern;
use ProxyManager\Configuration as ProxyManagerConfiguration;
use ProxyManager\Factory\LazyLoadingGhostFactory;
Expand All @@ -35,12 +37,11 @@
use ReflectionClass;
use Throwable;

use function array_diff_key;
use function array_intersect_key;
use function array_key_exists;
use function array_key_first;
use function class_exists;
use function count;
use function interface_exists;
use function is_array;
use function is_string;
use function trigger_deprecation;
use function trim;
Expand All @@ -56,11 +57,7 @@
* $dm = DocumentManager::create(new Connection(), $config);
*
* @phpstan-import-type CommitOptions from UnitOfWork
* @phpstan-type AutoEncryptionOptions array{
* keyVaultNamespace: string,
* kmsProviders: array<string, array<string, string>>,
* tlsOptions?: array{kmip: array{tlsCAFile: string, tlsCertificateKeyFile: string}},
* }
* @phpstan-type KmsProvider array{type: string, ...}
*/
class Configuration
{
Expand Down Expand Up @@ -133,7 +130,9 @@ class Configuration
* proxyDir?: string,
* proxyNamespace?: string,
* repositoryFactory?: RepositoryFactory,
* autoEncryption?: AutoEncryptionOptions,
* kmsProvider?: KmsProvider,
* defaultMasterKey?: array<string, mixed>|null,
* autoEncryption?: array<string, mixed>,
Comment on lines +133 to +135
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 extracted the kmsProvider and defaultMasterKey from the autoEncryption as they are not part of the driver options.

* }
*/
private array $attributes = [];
Expand Down Expand Up @@ -163,13 +162,34 @@ public function getDriverOptions(): array
],
];

if (isset($this->attributes['autoEncryption'])) {
$driverOptions['autoEncryption'] = $this->attributes['autoEncryption'];
if (isset($this->attributes['kmsProvider'])) {
$driverOptions['autoEncryption'] = $this->getAutoEncryptionOptions();
}

return $driverOptions;
}

/**
* Get options to create a ClientEncryption instance.
*
* @see https://www.php.net/manual/en/mongodb-driver-clientencryption.construct.php
*
* @return array{keyVaultClient?: Client|Manager, keyVaultNamespace: string, kmsProviders: array<string, mixed>, tlsOptions?: array<string, mixed>}
*/
public function getClientEncryptionOptions(): array
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 Configuration class is now responsible for creating the option arrays.

{
if (! isset($this->attributes['kmsProvider'])) {
throw ConfigurationException::clientEncryptionOptionsNotSet();
}

return array_intersect_key($this->getAutoEncryptionOptions(), [
'keyVaultClient' => 1,
'keyVaultNamespace' => 1,
'kmsProviders' => 1,
'tlsOptions' => 1,
]);
}

/**
* Adds a namespace under a certain alias.
*/
Expand Down Expand Up @@ -688,47 +708,72 @@ public function isLazyGhostObjectEnabled(): bool
}

/**
* Set the options for auto-encryption.
* Set the KMS provider to use for auto-encryption. The name of the KMS provider
* must be specified in the 'type' key of the array.
*
* @see https://www.php.net/manual/en/mongodb-driver-clientencryption.construct.php
*
* @phpstan-param AutoEncryptionOptions $options
*
* @throws InvalidArgumentException If the options are invalid.
* @param KmsProvider $kmsProvider
*/
public function setAutoEncryption(array $options): void
public function setKmsProvider(array $kmsProvider): void
{
if (! isset($options['keyVaultNamespace']) || ! is_string($options['keyVaultNamespace'])) {
throw new InvalidArgumentException('The "keyVaultNamespace" option is required.');
if (! isset($kmsProvider['type'])) {
throw ConfigurationException::kmsProviderTypeRequired();
}

// @todo Throw en exception if multiple KMS providers are defined. This is not supported yet and would require a setting for the KMS provider to use when creating a new collection
if (! isset($options['kmsProviders']) || ! is_array($options['kmsProviders']) || count($options['kmsProviders']) < 1) {
throw new InvalidArgumentException('The "kmsProviders" option is required.');
if (! is_string($kmsProvider['type'])) {
throw ConfigurationException::kmsProviderTypeMustBeString();
}

$this->attributes['autoEncryption'] = $options;
$this->attributes['kmsProvider'] = $kmsProvider;
}

/**
* Get the options for auto-encryption.
* Set the default master key to use when creating encrypted collections.
*
* @see https://www.php.net/manual/en/mongodb-driver-clientencryption.construct.php
* @param array<string, mixed>|null $masterKey
*/
public function setDefaultMasterKey(?array $masterKey): void
{
$this->attributes['defaultMasterKey'] = $masterKey;
}

/**
* Set the options for auto-encryption.
*
* @see https://www.php.net/manual/en/mongodb-driver-manager.construct.php
*
* @phpstan-return AutoEncryptionOptions
* @param array{ keyVaultClient?: Client|Manager, keyVaultNamespace?: string, tlsOptions?: array<string, mixed>, schemaMap?: array<string, mixed>, encryptedFieldsMap?: array<string, mixed>, extraOptions?: array<string, mixed>} $options
*/
public function getAutoEncryption(): ?array
public function setAutoEncryption(array $options): void
{
return $this->attributes['autoEncryption'] ?? null;
if (isset($options['kmsProviders'])) {
throw ConfigurationException::kmsProvidersOptionMustUseSetter();
}

$this->attributes['autoEncryption'] = $options;
}

public function getKmsProvider(): ?string
/**
* Get the default KMS provider name used when creating encrypted collections.
*/
public function getDefaultKmsProvider(): ?string
{
if (! isset($this->attributes['autoEncryption'])) {
return $this->attributes['kmsProvider']['type'] ?? null;
}

/**
* Get the default master key used when creating encrypted collections.
*
* @return array<string, mixed>|null
*/
public function getDefaultMasterKey(): ?array
{
if (! isset($this->attributes['kmsProvider']) || $this->attributes['kmsProvider']['type'] === 'local') {
return null;
}

return array_key_first($this->attributes['autoEncryption']['kmsProviders']);
return $this->attributes['defaultMasterKey'] ?? throw ConfigurationException::masterKeyRequired($this->attributes['kmsProvider']['type']);
}

private static function getVersion(): string
Expand All @@ -743,6 +788,16 @@ private static function getVersion(): string

return self::$version;
}

/** @return array<string, mixed> */
private function getAutoEncryptionOptions(): array
{
return [
'kmsProviders' => [$this->attributes['kmsProvider']['type'] => array_diff_key($this->attributes['kmsProvider'], ['type' => 0])],
'keyVaultNamespace' => $this->getDefaultDB() . '.datakeys',
...$this->attributes['autoEncryption'] ?? [],
];
}
}

interface_exists(MappingDriver::class);
27 changes: 27 additions & 0 deletions lib/Doctrine/ODM/MongoDB/ConfigurationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Exception;

use function sprintf;

final class ConfigurationException extends Exception
{
public static function persistentCollectionDirMissing(): self
Expand All @@ -27,4 +29,29 @@ public static function proxyDirMissing(): self
{
return new self('No proxy directory was configured. Please set a target directory first!');
}

public static function clientEncryptionOptionsNotSet(): self
{
return new self('MongoDB client encryption options are not set in configuration');
}

public static function kmsProviderTypeRequired(): self
{
return new self('The KMS provider "type" is required.');
}

public static function kmsProviderTypeMustBeString(): self
{
return new self('The KMS provider "type" must be a non-empty string.');
}

public static function kmsProvidersOptionMustUseSetter(): self
{
return new self('The "kmsProviders" encryption option must be set using the "setKmsProvider()" method.');
}

public static function masterKeyRequired(string $provider): self
{
return new self(sprintf('The "masterKey" configuration is required for the KMS provider "%s".', $provider));
}
}
14 changes: 7 additions & 7 deletions lib/Doctrine/ODM/MongoDB/DocumentManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,17 +223,17 @@ public function getClient(): Client
/** @internal */
public function getClientEncryption(): ClientEncryption
{
$autoEncryptionOptions = $this->config->getAutoEncryption();
if (isset($this->clientEncryption)) {
return $this->clientEncryption;
}

$options = $this->config->getClientEncryptionOptions();

if (! $autoEncryptionOptions) {
if (! $options) {
throw new RuntimeException('Auto-encryption is not enabled.');
}

return $this->clientEncryption ??= $this->client->createClientEncryption([
'keyVaultNamespace' => $autoEncryptionOptions['keyVaultNamespace'],
'kmsProviders' => $autoEncryptionOptions['kmsProviders'],
'tlsOptions' => $autoEncryptionOptions['tlsOptions'] ?? [],
]);
return $this->clientEncryption = $this->client->createClientEncryption($options);
}

/** Gets the metadata factory used to gather the metadata of classes. */
Expand Down
6 changes: 3 additions & 3 deletions lib/Doctrine/ODM/MongoDB/SchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ public function createDocumentCollection(string $documentName, ?int $maxTimeMs =
}

// Encryption is enabled only if the KMS provider is set and at least one field is encrypted
if ($this->dm->getConfiguration()->getKmsProvider()) {
if ($this->dm->getConfiguration()->getDefaultKmsProvider()) {
$encryptedFields = (new EncryptionFieldMap($this->dm->getMetadataFactory()))->getEncryptionFieldMap($class->name);

if ($encryptedFields) {
Expand All @@ -657,8 +657,8 @@ public function createDocumentCollection(string $documentName, ?int $maxTimeMs =
$this->dm->getDocumentDatabase($documentName)->createEncryptedCollection(
$class->getCollection(),
$this->dm->getClientEncryption(),
$this->dm->getConfiguration()->getKmsProvider(),
null, // @todo when is it necessary to set the master key?
$this->dm->getConfiguration()->getDefaultKmsProvider(),
$this->dm->getConfiguration()->getDefaultMasterKey(),
$this->getWriteOptions($maxTimeMs, $writeConcern, $options),
);
} else {
Expand Down
Loading