Skip to content

Commit bd5ac24

Browse files
authored
PHPLIB-1728: Encryption flag in driver metadata (#1809)
* Extract driver options into value objects So we can unit test options, as we're transforming some of the options that are passed * Use `DriverOptions` value object in Client * Prepend encryption flag `iue` to platform metadata Sending extra metadata in the handshake will allow us to detect when encryption is enabled. `iue` for In-Use Ecnryption was chosen to save bytes (as metadata will be truncated if it exceeds byte limit)
1 parent 82719c4 commit bd5ac24

File tree

7 files changed

+518
-148
lines changed

7 files changed

+518
-148
lines changed

psalm-baseline.xml

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -217,28 +217,6 @@
217217
<code><![CDATA[$query::NAME]]></code>
218218
</UndefinedConstant>
219219
</file>
220-
<file src="src/Client.php">
221-
<MixedArgument>
222-
<code><![CDATA[$driverOptions['driver'] ?? []]]></code>
223-
<code><![CDATA[$pipeline]]></code>
224-
</MixedArgument>
225-
<MixedArrayAssignment>
226-
<code><![CDATA[$options['kmsProviders'][$name]]]></code>
227-
</MixedArrayAssignment>
228-
<MixedAssignment>
229-
<code><![CDATA[$mergedDriver['platform']]]></code>
230-
<code><![CDATA[$provider]]></code>
231-
</MixedAssignment>
232-
<MixedPropertyTypeCoercion>
233-
<code><![CDATA[$driverOptions['builderEncoder'] ?? new BuilderEncoder()]]></code>
234-
</MixedPropertyTypeCoercion>
235-
<NamedArgumentNotAllowed>
236-
<code><![CDATA[$pipeline]]></code>
237-
</NamedArgumentNotAllowed>
238-
<PossiblyInvalidArgument>
239-
<code><![CDATA[$pipeline]]></code>
240-
</PossiblyInvalidArgument>
241-
</file>
242220
<file src="src/ClientBulkWrite.php">
243221
<PossiblyInvalidArgument>
244222
<code><![CDATA[$document]]></code>

psalm.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
<!-- This is often the result of type checks due to missing native types -->
4040
<DocblockTypeContradiction errorLevel="info" />
4141

42+
<!-- We still want to check types at runtime for users not using Psalm -->
43+
<RedundantConditionGivenDocblockType errorLevel="info" />
44+
4245
<!-- If the result of getenv is falsy, using the default URI is fine -->
4346
<RiskyTruthyFalsyComparison>
4447
<errorLevel type="suppress">

src/Client.php

Lines changed: 28 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,8 @@
1717

1818
namespace MongoDB;
1919

20-
use Composer\InstalledVersions;
2120
use Iterator;
22-
use MongoDB\BSON\Document;
23-
use MongoDB\BSON\PackedArray;
24-
use MongoDB\Builder\BuilderEncoder;
2521
use MongoDB\Builder\Pipeline;
26-
use MongoDB\Codec\Encoder;
2722
use MongoDB\Driver\BulkWriteCommand;
2823
use MongoDB\Driver\BulkWriteCommandResult;
2924
use MongoDB\Driver\ClientEncryption;
@@ -38,36 +33,24 @@
3833
use MongoDB\Exception\InvalidArgumentException;
3934
use MongoDB\Exception\UnexpectedValueException;
4035
use MongoDB\Exception\UnsupportedException;
41-
use MongoDB\Model\BSONArray;
42-
use MongoDB\Model\BSONDocument;
36+
use MongoDB\Model\AutoEncryptionOptions;
4337
use MongoDB\Model\DatabaseInfo;
38+
use MongoDB\Model\DriverOptions;
4439
use MongoDB\Operation\ClientBulkWriteCommand;
4540
use MongoDB\Operation\DropDatabase;
4641
use MongoDB\Operation\ListDatabaseNames;
4742
use MongoDB\Operation\ListDatabases;
4843
use MongoDB\Operation\Watch;
4944
use stdClass;
5045
use Stringable;
51-
use Throwable;
5246

5347
use function array_diff_key;
54-
use function is_array;
55-
use function is_string;
5648

49+
/** @psalm-import-type stage from Builder\Pipeline */
5750
class Client implements Stringable
5851
{
5952
public const DEFAULT_URI = 'mongodb://127.0.0.1/';
6053

61-
private const DEFAULT_TYPE_MAP = [
62-
'array' => BSONArray::class,
63-
'document' => BSONDocument::class,
64-
'root' => BSONDocument::class,
65-
];
66-
67-
private const HANDSHAKE_SEPARATOR = '/';
68-
69-
private static ?string $version = null;
70-
7154
private Manager $manager;
7255

7356
private ReadConcern $readConcern;
@@ -76,14 +59,9 @@ class Client implements Stringable
7659

7760
private string $uri;
7861

79-
private array $typeMap;
80-
81-
/** @psalm-var Encoder<array|stdClass|Document|PackedArray, mixed> */
82-
private readonly Encoder $builderEncoder;
83-
8462
private WriteConcern $writeConcern;
8563

86-
private bool $autoEncryptionEnabled;
64+
private DriverOptions $driverOptions;
8765

8866
/**
8967
* Constructs a new Client instance.
@@ -113,34 +91,14 @@ class Client implements Stringable
11391
*/
11492
public function __construct(?string $uri = null, array $uriOptions = [], array $driverOptions = [])
11593
{
116-
$driverOptions += ['typeMap' => self::DEFAULT_TYPE_MAP];
117-
118-
if (! is_array($driverOptions['typeMap'])) {
119-
throw InvalidArgumentException::invalidType('"typeMap" driver option', $driverOptions['typeMap'], 'array');
120-
}
121-
122-
if (isset($driverOptions['autoEncryption']) && is_array($driverOptions['autoEncryption'])) {
123-
$driverOptions['autoEncryption'] = $this->prepareEncryptionOptions($driverOptions['autoEncryption']);
124-
}
125-
126-
if (isset($driverOptions['builderEncoder']) && ! $driverOptions['builderEncoder'] instanceof Encoder) {
127-
throw InvalidArgumentException::invalidType('"builderEncoder" option', $driverOptions['builderEncoder'], Encoder::class);
128-
}
129-
130-
$driverOptions['driver'] = $this->mergeDriverInfo($driverOptions['driver'] ?? []);
94+
$this->driverOptions = DriverOptions::fromArray($driverOptions);
13195

13296
$this->uri = $uri ?? self::DEFAULT_URI;
133-
$this->builderEncoder = $driverOptions['builderEncoder'] ?? new BuilderEncoder();
134-
$this->typeMap = $driverOptions['typeMap'];
135-
136-
/* Database and Collection objects may need to know whether auto
137-
* encryption is enabled for dropping collections. Track this via an
138-
* internal option until PHPC-2615 is implemented. */
139-
$this->autoEncryptionEnabled = isset($driverOptions['autoEncryption']['keyVaultNamespace']);
14097

141-
$driverOptions = array_diff_key($driverOptions, ['builderEncoder' => 1, 'typeMap' => 1]);
98+
$driverOptions = array_diff_key($this->driverOptions->toArray(), ['builderEncoder' => 1, 'typeMap' => 1]);
14299

143100
$this->manager = new Manager($uri, $uriOptions, $driverOptions);
101+
144102
$this->readConcern = $this->manager->getReadConcern();
145103
$this->readPreference = $this->manager->getReadPreference();
146104
$this->writeConcern = $this->manager->getWriteConcern();
@@ -156,8 +114,8 @@ public function __debugInfo(): array
156114
return [
157115
'manager' => $this->manager,
158116
'uri' => $this->uri,
159-
'typeMap' => $this->typeMap,
160-
'builderEncoder' => $this->builderEncoder,
117+
'typeMap' => $this->driverOptions->typeMap,
118+
'builderEncoder' => $this->driverOptions->builderEncoder,
161119
'writeConcern' => $this->writeConcern,
162120
];
163121
}
@@ -225,13 +183,13 @@ public function bulkWrite(BulkWriteCommand|ClientBulkWrite $bulk, array $options
225183
/**
226184
* Returns a ClientEncryption instance for explicit encryption and decryption
227185
*
228-
* @param array $options Encryption options
186+
* @param array{kmsProviders?: stdClass|array<string, array>, keyVaultClient?: Client|Manager} $options
229187
*/
230188
public function createClientEncryption(array $options): ClientEncryption
231189
{
232-
$options = $this->prepareEncryptionOptions($options);
190+
$options = AutoEncryptionOptions::fromArray($options);
233191

234-
return $this->manager->createClientEncryption($options);
192+
return $this->manager->createClientEncryption($options->toArray());
235193
}
236194

237195
/**
@@ -268,7 +226,11 @@ public function dropDatabase(string $databaseName, array $options = []): void
268226
*/
269227
public function getCollection(string $databaseName, string $collectionName, array $options = []): Collection
270228
{
271-
$options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder, 'autoEncryptionEnabled' => $this->autoEncryptionEnabled];
229+
$options += [
230+
'typeMap' => $this->driverOptions->typeMap,
231+
'builderEncoder' => $this->driverOptions->builderEncoder,
232+
'autoEncryptionEnabled' => $this->driverOptions->isAutoEncryptionEnabled(),
233+
];
272234

273235
return new Collection($this->manager, $databaseName, $collectionName, $options);
274236
}
@@ -283,7 +245,11 @@ public function getCollection(string $databaseName, string $collectionName, arra
283245
*/
284246
public function getDatabase(string $databaseName, array $options = []): Database
285247
{
286-
$options += ['typeMap' => $this->typeMap, 'builderEncoder' => $this->builderEncoder, 'autoEncryptionEnabled' => $this->autoEncryptionEnabled];
248+
$options += [
249+
'typeMap' => $this->driverOptions->typeMap,
250+
'builderEncoder' => $this->driverOptions->builderEncoder,
251+
'autoEncryptionEnabled' => $this->driverOptions->isAutoEncryptionEnabled(),
252+
];
287253

288254
return new Database($this->manager, $databaseName, $options);
289255
}
@@ -319,7 +285,7 @@ public function getReadPreference(): ReadPreference
319285
*/
320286
public function getTypeMap(): array
321287
{
322-
return $this->typeMap;
288+
return $this->driverOptions->typeMap;
323289
}
324290

325291
/**
@@ -418,8 +384,8 @@ public function startSession(array $options = []): Session
418384
* Create a change stream for watching changes to the cluster.
419385
*
420386
* @see Watch::__construct() for supported options
421-
* @param array $pipeline Aggregation pipeline
422-
* @param array $options Command options
387+
* @psalm-param list<stage> $pipeline Aggregation pipeline
388+
* @param array $options Command options
423389
* @throws InvalidArgumentException for parameter/option parsing errors
424390
*/
425391
public function watch(array $pipeline = [], array $options = []): ChangeStream
@@ -428,7 +394,8 @@ public function watch(array $pipeline = [], array $options = []): ChangeStream
428394
$pipeline = new Pipeline(...$pipeline);
429395
}
430396

431-
$pipeline = $this->builderEncoder->encodeIfSupported($pipeline);
397+
/** @var array<array-key, mixed> $pipeline */
398+
$pipeline = $this->driverOptions->builderEncoder->encodeIfSupported($pipeline);
432399

433400
if (! isset($options['readPreference']) && ! is_in_transaction($options)) {
434401
$options['readPreference'] = $this->readPreference;
@@ -441,76 +408,11 @@ public function watch(array $pipeline = [], array $options = []): ChangeStream
441408
}
442409

443410
if (! isset($options['typeMap'])) {
444-
$options['typeMap'] = $this->typeMap;
411+
$options['typeMap'] = $this->driverOptions->typeMap;
445412
}
446413

447414
$operation = new Watch($this->manager, null, null, $pipeline, $options);
448415

449416
return $operation->execute($server);
450417
}
451-
452-
private static function getVersion(): string
453-
{
454-
if (self::$version === null) {
455-
try {
456-
self::$version = InstalledVersions::getPrettyVersion('mongodb/mongodb') ?? 'unknown';
457-
} catch (Throwable) {
458-
self::$version = 'error';
459-
}
460-
}
461-
462-
return self::$version;
463-
}
464-
465-
private function mergeDriverInfo(array $driver): array
466-
{
467-
$mergedDriver = [
468-
'name' => 'PHPLIB',
469-
'version' => self::getVersion(),
470-
];
471-
472-
if (isset($driver['name'])) {
473-
if (! is_string($driver['name'])) {
474-
throw InvalidArgumentException::invalidType('"name" handshake option', $driver['name'], 'string');
475-
}
476-
477-
$mergedDriver['name'] .= self::HANDSHAKE_SEPARATOR . $driver['name'];
478-
}
479-
480-
if (isset($driver['version'])) {
481-
if (! is_string($driver['version'])) {
482-
throw InvalidArgumentException::invalidType('"version" handshake option', $driver['version'], 'string');
483-
}
484-
485-
$mergedDriver['version'] .= self::HANDSHAKE_SEPARATOR . $driver['version'];
486-
}
487-
488-
if (isset($driver['platform'])) {
489-
$mergedDriver['platform'] = $driver['platform'];
490-
}
491-
492-
return $mergedDriver;
493-
}
494-
495-
private function prepareEncryptionOptions(array $options): array
496-
{
497-
if (isset($options['keyVaultClient'])) {
498-
if ($options['keyVaultClient'] instanceof self) {
499-
$options['keyVaultClient'] = $options['keyVaultClient']->manager;
500-
} elseif (! $options['keyVaultClient'] instanceof Manager) {
501-
throw InvalidArgumentException::invalidType('"keyVaultClient" option', $options['keyVaultClient'], [self::class, Manager::class]);
502-
}
503-
}
504-
505-
// The server requires an empty document for automatic credentials.
506-
if (isset($options['kmsProviders']) && is_array($options['kmsProviders'])) {
507-
foreach ($options['kmsProviders'] as $name => $provider) {
508-
if ($provider === []) {
509-
$options['kmsProviders'][$name] = new stdClass();
510-
}
511-
}
512-
}
513-
514-
return $options;
515-
}
516418
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace MongoDB\Model;
4+
5+
use MongoDB\Client;
6+
use MongoDB\Driver\Manager;
7+
use MongoDB\Exception\InvalidArgumentException;
8+
use stdClass;
9+
10+
use function array_diff_key;
11+
use function array_filter;
12+
use function is_array;
13+
use function sprintf;
14+
15+
/** @internal */
16+
final class AutoEncryptionOptions
17+
{
18+
private const KEY_KEY_VAULT_CLIENT = 'keyVaultClient';
19+
private const KEY_KMS_PROVIDERS = 'kmsProviders';
20+
21+
private function __construct(
22+
private readonly ?Manager $keyVaultClient,
23+
private readonly array|stdClass|null $kmsProviders,
24+
private readonly array $miscOptions,
25+
) {
26+
}
27+
28+
/** @param array{kmsProviders?: stdClass|array<string, array>, keyVaultClient?: Client|Manager} $options */
29+
public static function fromArray(array $options): self
30+
{
31+
// The server requires an empty document for automatic credentials.
32+
if (isset($options[self::KEY_KMS_PROVIDERS]) && is_array($options[self::KEY_KMS_PROVIDERS])) {
33+
foreach ($options[self::KEY_KMS_PROVIDERS] as $name => $provider) {
34+
if ($provider === []) {
35+
$options[self::KEY_KMS_PROVIDERS][$name] = new stdClass();
36+
}
37+
}
38+
}
39+
40+
$keyVaultClient = $options[self::KEY_KEY_VAULT_CLIENT] ?? null;
41+
42+
if ($keyVaultClient !== null && ! $keyVaultClient instanceof Client && ! $keyVaultClient instanceof Manager) {
43+
throw InvalidArgumentException::invalidType(
44+
sprintf('"%s" option', self::KEY_KEY_VAULT_CLIENT),
45+
$keyVaultClient,
46+
[Client::class, Manager::class],
47+
);
48+
}
49+
50+
return new self(
51+
keyVaultClient: $keyVaultClient instanceof Client ? $keyVaultClient->getManager() : $keyVaultClient,
52+
kmsProviders: $options[self::KEY_KMS_PROVIDERS] ?? null,
53+
miscOptions: array_diff_key($options, [self::KEY_KEY_VAULT_CLIENT => 1, self::KEY_KMS_PROVIDERS => 1]),
54+
);
55+
}
56+
57+
public function toArray(): array
58+
{
59+
return array_filter(
60+
[
61+
self::KEY_KEY_VAULT_CLIENT => $this->keyVaultClient,
62+
self::KEY_KMS_PROVIDERS => $this->kmsProviders,
63+
] + $this->miscOptions,
64+
static fn ($option) => $option !== null,
65+
);
66+
}
67+
}

0 commit comments

Comments
 (0)