Skip to content

Commit 5506175

Browse files
committed
PHPLIB-1122: Support BSON objects for encryptedFields option
CreateEncryptedCollection::createDataKeys() required more intensive changes to support BSON objects at various levels. The handling for PackedArray and array-yielding Serializable objects is modeled after document_to_array(). Tests were intentionally omitted for the state collection names, since custom values are unsupported and the options aren't documented for public use. Functional tests such as asserting options in outgoing 'create' commands seem like overkill.
1 parent 3c4e101 commit 5506175

File tree

4 files changed

+117
-21
lines changed

4 files changed

+117
-21
lines changed

psalm-baseline.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,9 @@
422422
<code>! is_array($encryptedFields['fields'])</code>
423423
<code>! is_array($field) &amp;&amp; ! is_object($field)</code>
424424
</DocblockTypeContradiction>
425+
<MixedArgument occurrences="1">
426+
<code>$this-&gt;options['encryptedFields']</code>
427+
</MixedArgument>
425428
</file>
426429
<file src="src/Operation/CreateIndexes.php">
427430
<DocblockTypeContradiction occurrences="1">

src/Operation/CreateEncryptedCollection.php

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
namespace MongoDB\Operation;
1919

2020
use MongoDB\BSON\Binary;
21+
use MongoDB\BSON\PackedArray;
22+
use MongoDB\BSON\Serializable;
2123
use MongoDB\Driver\ClientEncryption;
2224
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
2325
use MongoDB\Driver\Server;
@@ -27,6 +29,7 @@
2729
use function array_key_exists;
2830
use function is_array;
2931
use function is_object;
32+
use function MongoDB\document_to_array;
3033
use function MongoDB\server_supports_feature;
3134

3235
/**
@@ -87,7 +90,7 @@ public function __construct(string $databaseName, string $collectionName, array
8790
$this->createCollection = new CreateCollection($databaseName, $collectionName, $options);
8891

8992
/** @psalm-var array{ecocCollection?: ?string, escCollection?: ?string} */
90-
$encryptedFields = (array) $options['encryptedFields'];
93+
$encryptedFields = document_to_array($options['encryptedFields']);
9194
$enxcolOptions = ['clusteredIndex' => ['key' => ['_id' => 1], 'unique' => true]];
9295

9396
$this->createMetadataCollections = [
@@ -118,12 +121,28 @@ public function __construct(string $databaseName, string $collectionName, array
118121
*/
119122
public function createDataKeys(ClientEncryption $clientEncryption, string $kmsProvider, ?array $masterKey, ?array &$encryptedFields = null): void
120123
{
121-
/** @psalm-var array{fields: list<array{keyId: ?Binary}|object{keyId: ?Binary}>} */
122-
$encryptedFields = (array) $this->options['encryptedFields'];
124+
/** @psalm-var array{fields: list<array{keyId: ?Binary}|object{keyId: ?Binary}>|Serializable|PackedArray} */
125+
$encryptedFields = document_to_array($this->options['encryptedFields']);
123126

124-
/* NOP if there are no fields to examine. If the type is invalid, defer
125-
* to the server to raise an error in execute(). */
126-
if (! isset($encryptedFields['fields']) || ! is_array($encryptedFields['fields'])) {
127+
// NOP if there are no fields to examine
128+
if (! isset($encryptedFields['fields'])) {
129+
return;
130+
}
131+
132+
// Allow PackedArray or Serializable object for the fields array
133+
if ($encryptedFields['fields'] instanceof PackedArray) {
134+
/** @psalm-var array */
135+
$encryptedFields['fields'] = $encryptedFields['fields']->toPHP([
136+
'array' => 'array',
137+
'document' => 'object',
138+
'root' => 'array',
139+
]);
140+
} elseif ($encryptedFields['fields'] instanceof Serializable) {
141+
$encryptedFields['fields'] = $encryptedFields['fields']->bsonSerialize();
142+
}
143+
144+
// Skip invalid types and defer to the server to raise an error
145+
if (! is_array($encryptedFields['fields'])) {
127146
return;
128147
}
129148

@@ -138,7 +157,7 @@ public function createDataKeys(ClientEncryption $clientEncryption, string $kmsPr
138157
continue;
139158
}
140159

141-
$field = (array) $field;
160+
$field = document_to_array($field);
142161

143162
if (array_key_exists('keyId', $field) && $field['keyId'] === null) {
144163
$field['keyId'] = $clientEncryption->createDataKey(...$createDataKeyArgs);

src/Operation/DropEncryptedCollection.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
use function is_array;
2525
use function is_object;
26+
use function MongoDB\document_to_array;
2627

2728
/**
2829
* Drop an encrypted collection.
@@ -72,7 +73,7 @@ public function __construct(string $databaseName, string $collectionName, array
7273
}
7374

7475
/** @psalm-var array{ecocCollection?: ?string, escCollection?: ?string} */
75-
$encryptedFields = (array) $options['encryptedFields'];
76+
$encryptedFields = document_to_array($options['encryptedFields']);
7677

7778
$this->dropMetadataCollections = [
7879
new DropCollection($databaseName, $encryptedFields['escCollection'] ?? 'enxcol_.' . $collectionName . '.esc'),

tests/Operation/CreateEncryptedCollectionFunctionalTest.php

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
namespace MongoDB\Tests\Operation;
44

55
use MongoDB\BSON\Binary;
6+
use MongoDB\BSON\Document;
67
use MongoDB\Client;
78
use MongoDB\ClientEncryption;
89
use MongoDB\Driver\WriteConcern;
10+
use MongoDB\Model\BSONArray;
11+
use MongoDB\Model\BSONDocument;
912
use MongoDB\Operation\CreateEncryptedCollection;
1013

1114
use function base64_decode;
@@ -61,61 +64,100 @@ public function setUp(): void
6164
]);
6265
}
6366

64-
public function testCreateDataKeysNopIfFieldsArrayIsMissing(): void
67+
/** @dataProvider provideEncryptedFieldsAndFieldsIsMissing */
68+
public function testCreateDataKeysNopIfFieldsIsMissing($input, array $expectedOutput): void
6569
{
6670
$operation = new CreateEncryptedCollection(
6771
$this->getDatabaseName(),
6872
$this->getCollectionName(),
69-
['encryptedFields' => []]
73+
['encryptedFields' => $input]
7074
);
7175

7276
$operation->createDataKeys(
7377
$this->clientEncryption,
7478
'local',
7579
null,
76-
$encryptedFields
80+
$encryptedFieldsOutput
7781
);
7882

79-
$this->assertSame([], $encryptedFields);
83+
$this->assertSame($expectedOutput, $encryptedFieldsOutput);
8084
}
8185

82-
public function testCreateDataKeysNopIfFieldsArrayIsInvalid(): void
86+
public function provideEncryptedFieldsAndFieldsIsMissing(): array
87+
{
88+
$ef = [];
89+
90+
return [
91+
'array' => [$ef, $ef],
92+
'object' => [(object) $ef, $ef],
93+
'Serializable' => [new BSONDocument($ef), $ef],
94+
'Document' => [Document::fromPHP($ef), $ef],
95+
];
96+
}
97+
98+
/** @dataProvider provideEncryptedFieldsAndFieldsHasInvalidType */
99+
public function testCreateDataKeysNopIfFieldsHasInvalidType($input, array $expectedOutput): void
83100
{
84101
$operation = new CreateEncryptedCollection(
85102
$this->getDatabaseName(),
86103
$this->getCollectionName(),
87-
['encryptedFields' => ['fields' => 'not-an-array']]
104+
['encryptedFields' => $input]
88105
);
89106

90107
$operation->createDataKeys(
91108
$this->clientEncryption,
92109
'local',
93110
null,
94-
$encryptedFields
111+
$encryptedFieldsOutput
95112
);
96113

97-
$this->assertSame(['fields' => 'not-an-array'], $encryptedFields);
114+
$this->assertSame($expectedOutput, $encryptedFieldsOutput);
98115
}
99116

100-
public function testCreateDataKeysSkipsNonDocumentFields(): void
117+
public function provideEncryptedFieldsAndFieldsHasInvalidType(): array
118+
{
119+
$ef = ['fields' => 'not-an-array'];
120+
121+
return [
122+
'array' => [$ef, $ef],
123+
'object' => [(object) $ef, $ef],
124+
'Serializable' => [new BSONDocument($ef), $ef],
125+
'Document' => [Document::fromPHP($ef), $ef],
126+
];
127+
}
128+
129+
/** @dataProvider provideEncryptedFieldsElementHasInvalidType */
130+
public function testCreateDataKeysSkipsNonDocumentFields($input, array $expectedOutput): void
101131
{
102132
$operation = new CreateEncryptedCollection(
103133
$this->getDatabaseName(),
104134
$this->getCollectionName(),
105-
['encryptedFields' => ['fields' => ['not-an-array-or-object']]]
135+
['encryptedFields' => $input],
106136
);
107137

108138
$operation->createDataKeys(
109139
$this->clientEncryption,
110140
'local',
111141
null,
112-
$encryptedFields
142+
$encryptedFieldsOutput
113143
);
114144

115-
$this->assertSame(['fields' => ['not-an-array-or-object']], $encryptedFields);
145+
$this->assertSame($expectedOutput, $encryptedFieldsOutput);
146+
}
147+
148+
public function provideEncryptedFieldsElementHasInvalidType(): array
149+
{
150+
$ef = ['fields' => ['not-an-array-or-object']];
151+
152+
return [
153+
'array' => [$ef, $ef],
154+
'object' => [(object) $ef, $ef],
155+
'Serializable' => [new BSONDocument(['fields' => new BSONArray(['not-an-array-or-object'])]), $ef],
156+
'Document' => [Document::fromPHP($ef), $ef],
157+
];
116158
}
117159

118-
public function testCreateDataKeysDoesNotModifyEncryptedFieldsObjectOption(): void
160+
public function testCreateDataKeysDoesNotModifyOriginalEncryptedFieldsOption(): void
119161
{
120162
$originalField = (object) ['path' => 'ssn', 'bsonType' => 'string', 'keyId' => null];
121163
$originalEncryptedFields = (object) ['fields' => [$originalField]];
@@ -139,6 +181,37 @@ public function testCreateDataKeysDoesNotModifyEncryptedFieldsObjectOption(): vo
139181
$this->assertInstanceOf(Binary::class, $modifiedEncryptedFields['fields'][0]['keyId'] ?? null);
140182
}
141183

184+
/** @dataProvider provideEncryptedFields */
185+
public function testEncryptedFieldsDocuments($input): void
186+
{
187+
$operation = new CreateEncryptedCollection(
188+
$this->getDatabaseName(),
189+
$this->getCollectionName(),
190+
['encryptedFields' => $input]
191+
);
192+
193+
$operation->createDataKeys(
194+
$this->clientEncryption,
195+
'local',
196+
null,
197+
$modifiedEncryptedFields
198+
);
199+
200+
$this->assertInstanceOf(Binary::class, $modifiedEncryptedFields['fields'][0]['keyId'] ?? null);
201+
}
202+
203+
public function provideEncryptedFields(): array
204+
{
205+
$ef = ['fields' => [['path' => 'ssn', 'bsonType' => 'string', 'keyId' => null]]];
206+
207+
return [
208+
'array' => [$ef],
209+
'object' => [(object) $ef],
210+
'Serializable' => [new BSONDocument(['fields' => new BSONArray([new BSONDocument($ef['fields'][0])])])],
211+
'Document' => [Document::fromPHP($ef)],
212+
];
213+
}
214+
142215
public static function createTestClient(?string $uri = null, array $options = [], array $driverOptions = []): Client
143216
{
144217
if (isset($driverOptions['autoEncryption']) && getenv('CRYPT_SHARED_LIB_PATH')) {

0 commit comments

Comments
 (0)