Skip to content

Commit 64cb384

Browse files
authored
[Encryption] Set master key when creating an encrypted collection (#2780)
1 parent 72ee408 commit 64cb384

File tree

6 files changed

+246
-47
lines changed

6 files changed

+246
-47
lines changed

lib/Doctrine/ODM/MongoDB/Configuration.php

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use InvalidArgumentException;
2727
use Jean85\PrettyVersions;
2828
use LogicException;
29+
use MongoDB\Client;
30+
use MongoDB\Driver\Manager;
2931
use MongoDB\Driver\WriteConcern;
3032
use ProxyManager\Configuration as ProxyManagerConfiguration;
3133
use ProxyManager\Factory\LazyLoadingGhostFactory;
@@ -35,12 +37,11 @@
3537
use ReflectionClass;
3638
use Throwable;
3739

40+
use function array_diff_key;
41+
use function array_intersect_key;
3842
use function array_key_exists;
39-
use function array_key_first;
4043
use function class_exists;
41-
use function count;
4244
use function interface_exists;
43-
use function is_array;
4445
use function is_string;
4546
use function trigger_deprecation;
4647
use function trim;
@@ -56,11 +57,7 @@
5657
* $dm = DocumentManager::create(new Connection(), $config);
5758
*
5859
* @phpstan-import-type CommitOptions from UnitOfWork
59-
* @phpstan-type AutoEncryptionOptions array{
60-
* keyVaultNamespace: string,
61-
* kmsProviders: array<string, array<string, string>>,
62-
* tlsOptions?: array{kmip: array{tlsCAFile: string, tlsCertificateKeyFile: string}},
63-
* }
60+
* @phpstan-type KmsProvider array{type: string, ...}
6461
*/
6562
class Configuration
6663
{
@@ -133,7 +130,9 @@ class Configuration
133130
* proxyDir?: string,
134131
* proxyNamespace?: string,
135132
* repositoryFactory?: RepositoryFactory,
136-
* autoEncryption?: AutoEncryptionOptions,
133+
* kmsProvider?: KmsProvider,
134+
* defaultMasterKey?: array<string, mixed>|null,
135+
* autoEncryption?: array<string, mixed>,
137136
* }
138137
*/
139138
private array $attributes = [];
@@ -163,13 +162,34 @@ public function getDriverOptions(): array
163162
],
164163
];
165164

166-
if (isset($this->attributes['autoEncryption'])) {
167-
$driverOptions['autoEncryption'] = $this->attributes['autoEncryption'];
165+
if (isset($this->attributes['kmsProvider'])) {
166+
$driverOptions['autoEncryption'] = $this->getAutoEncryptionOptions();
168167
}
169168

170169
return $driverOptions;
171170
}
172171

172+
/**
173+
* Get options to create a ClientEncryption instance.
174+
*
175+
* @see https://www.php.net/manual/en/mongodb-driver-clientencryption.construct.php
176+
*
177+
* @return array{keyVaultClient?: Client|Manager, keyVaultNamespace: string, kmsProviders: array<string, mixed>, tlsOptions?: array<string, mixed>}
178+
*/
179+
public function getClientEncryptionOptions(): array
180+
{
181+
if (! isset($this->attributes['kmsProvider'])) {
182+
throw ConfigurationException::clientEncryptionOptionsNotSet();
183+
}
184+
185+
return array_intersect_key($this->getAutoEncryptionOptions(), [
186+
'keyVaultClient' => 1,
187+
'keyVaultNamespace' => 1,
188+
'kmsProviders' => 1,
189+
'tlsOptions' => 1,
190+
]);
191+
}
192+
173193
/**
174194
* Adds a namespace under a certain alias.
175195
*/
@@ -688,47 +708,72 @@ public function isLazyGhostObjectEnabled(): bool
688708
}
689709

690710
/**
691-
* Set the options for auto-encryption.
711+
* Set the KMS provider to use for auto-encryption. The name of the KMS provider
712+
* must be specified in the 'type' key of the array.
692713
*
693714
* @see https://www.php.net/manual/en/mongodb-driver-clientencryption.construct.php
694715
*
695-
* @phpstan-param AutoEncryptionOptions $options
696-
*
697-
* @throws InvalidArgumentException If the options are invalid.
716+
* @param KmsProvider $kmsProvider
698717
*/
699-
public function setAutoEncryption(array $options): void
718+
public function setKmsProvider(array $kmsProvider): void
700719
{
701-
if (! isset($options['keyVaultNamespace']) || ! is_string($options['keyVaultNamespace'])) {
702-
throw new InvalidArgumentException('The "keyVaultNamespace" option is required.');
720+
if (! isset($kmsProvider['type'])) {
721+
throw ConfigurationException::kmsProviderTypeRequired();
703722
}
704723

705-
// @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
706-
if (! isset($options['kmsProviders']) || ! is_array($options['kmsProviders']) || count($options['kmsProviders']) < 1) {
707-
throw new InvalidArgumentException('The "kmsProviders" option is required.');
724+
if (! is_string($kmsProvider['type'])) {
725+
throw ConfigurationException::kmsProviderTypeMustBeString();
708726
}
709727

710-
$this->attributes['autoEncryption'] = $options;
728+
$this->attributes['kmsProvider'] = $kmsProvider;
711729
}
712730

713731
/**
714-
* Get the options for auto-encryption.
732+
* Set the default master key to use when creating encrypted collections.
715733
*
716-
* @see https://www.php.net/manual/en/mongodb-driver-clientencryption.construct.php
734+
* @param array<string, mixed>|null $masterKey
735+
*/
736+
public function setDefaultMasterKey(?array $masterKey): void
737+
{
738+
$this->attributes['defaultMasterKey'] = $masterKey;
739+
}
740+
741+
/**
742+
* Set the options for auto-encryption.
743+
*
744+
* @see https://www.php.net/manual/en/mongodb-driver-manager.construct.php
717745
*
718-
* @phpstan-return AutoEncryptionOptions
746+
* @param array{ keyVaultClient?: Client|Manager, keyVaultNamespace?: string, tlsOptions?: array<string, mixed>, schemaMap?: array<string, mixed>, encryptedFieldsMap?: array<string, mixed>, extraOptions?: array<string, mixed>} $options
719747
*/
720-
public function getAutoEncryption(): ?array
748+
public function setAutoEncryption(array $options): void
721749
{
722-
return $this->attributes['autoEncryption'] ?? null;
750+
if (isset($options['kmsProviders'])) {
751+
throw ConfigurationException::kmsProvidersOptionMustUseSetter();
752+
}
753+
754+
$this->attributes['autoEncryption'] = $options;
723755
}
724756

725-
public function getKmsProvider(): ?string
757+
/**
758+
* Get the default KMS provider name used when creating encrypted collections.
759+
*/
760+
public function getDefaultKmsProvider(): ?string
726761
{
727-
if (! isset($this->attributes['autoEncryption'])) {
762+
return $this->attributes['kmsProvider']['type'] ?? null;
763+
}
764+
765+
/**
766+
* Get the default master key used when creating encrypted collections.
767+
*
768+
* @return array<string, mixed>|null
769+
*/
770+
public function getDefaultMasterKey(): ?array
771+
{
772+
if (! isset($this->attributes['kmsProvider']) || $this->attributes['kmsProvider']['type'] === 'local') {
728773
return null;
729774
}
730775

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

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

744789
return self::$version;
745790
}
791+
792+
/** @return array<string, mixed> */
793+
private function getAutoEncryptionOptions(): array
794+
{
795+
return [
796+
'kmsProviders' => [$this->attributes['kmsProvider']['type'] => array_diff_key($this->attributes['kmsProvider'], ['type' => 0])],
797+
'keyVaultNamespace' => $this->getDefaultDB() . '.datakeys',
798+
...$this->attributes['autoEncryption'] ?? [],
799+
];
800+
}
746801
}
747802

748803
interface_exists(MappingDriver::class);

lib/Doctrine/ODM/MongoDB/ConfigurationException.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Exception;
88

9+
use function sprintf;
10+
911
final class ConfigurationException extends Exception
1012
{
1113
public static function persistentCollectionDirMissing(): self
@@ -27,4 +29,29 @@ public static function proxyDirMissing(): self
2729
{
2830
return new self('No proxy directory was configured. Please set a target directory first!');
2931
}
32+
33+
public static function clientEncryptionOptionsNotSet(): self
34+
{
35+
return new self('MongoDB client encryption options are not set in configuration');
36+
}
37+
38+
public static function kmsProviderTypeRequired(): self
39+
{
40+
return new self('The KMS provider "type" is required.');
41+
}
42+
43+
public static function kmsProviderTypeMustBeString(): self
44+
{
45+
return new self('The KMS provider "type" must be a non-empty string.');
46+
}
47+
48+
public static function kmsProvidersOptionMustUseSetter(): self
49+
{
50+
return new self('The "kmsProviders" encryption option must be set using the "setKmsProvider()" method.');
51+
}
52+
53+
public static function masterKeyRequired(string $provider): self
54+
{
55+
return new self(sprintf('The "masterKey" configuration is required for the KMS provider "%s".', $provider));
56+
}
3057
}

lib/Doctrine/ODM/MongoDB/DocumentManager.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,17 +223,17 @@ public function getClient(): Client
223223
/** @internal */
224224
public function getClientEncryption(): ClientEncryption
225225
{
226-
$autoEncryptionOptions = $this->config->getAutoEncryption();
226+
if (isset($this->clientEncryption)) {
227+
return $this->clientEncryption;
228+
}
229+
230+
$options = $this->config->getClientEncryptionOptions();
227231

228-
if (! $autoEncryptionOptions) {
232+
if (! $options) {
229233
throw new RuntimeException('Auto-encryption is not enabled.');
230234
}
231235

232-
return $this->clientEncryption ??= $this->client->createClientEncryption([
233-
'keyVaultNamespace' => $autoEncryptionOptions['keyVaultNamespace'],
234-
'kmsProviders' => $autoEncryptionOptions['kmsProviders'],
235-
'tlsOptions' => $autoEncryptionOptions['tlsOptions'] ?? [],
236-
]);
236+
return $this->clientEncryption = $this->client->createClientEncryption($options);
237237
}
238238

239239
/** Gets the metadata factory used to gather the metadata of classes. */

lib/Doctrine/ODM/MongoDB/SchemaManager.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ public function createDocumentCollection(string $documentName, ?int $maxTimeMs =
645645
}
646646

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

651651
if ($encryptedFields) {
@@ -657,8 +657,8 @@ public function createDocumentCollection(string $documentName, ?int $maxTimeMs =
657657
$this->dm->getDocumentDatabase($documentName)->createEncryptedCollection(
658658
$class->getCollection(),
659659
$this->dm->getClientEncryption(),
660-
$this->dm->getConfiguration()->getKmsProvider(),
661-
null, // @todo when is it necessary to set the master key?
660+
$this->dm->getConfiguration()->getDefaultKmsProvider(),
661+
$this->dm->getConfiguration()->getDefaultMasterKey(),
662662
$this->getWriteOptions($maxTimeMs, $writeConcern, $options),
663663
);
664664
} else {

0 commit comments

Comments
 (0)