Skip to content

Commit 6bc7fa2

Browse files
authored
PHPLIB-863: Queryable encryption example for docs team (#939)
Introduces an isEnterprise() helper, since this example requires automatic encryption.
1 parent bb45c03 commit 6bc7fa2

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

tests/DocumentationExamplesTest.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace MongoDB\Tests;
44

5+
use MongoDB\BSON\Binary;
56
use MongoDB\BSON\ObjectId;
67
use MongoDB\BSON\UTCDateTime;
78
use MongoDB\Collection;
@@ -11,7 +12,9 @@
1112
use MongoDB\Driver\Exception\Exception;
1213
use MongoDB\Driver\ReadPreference;
1314
use MongoDB\Driver\WriteConcern;
15+
use MongoDB\Tests\SpecTests\ClientSideEncryptionSpecTest;
1416

17+
use function base64_decode;
1518
use function in_array;
1619
use function microtime;
1720
use function ob_end_clean;
@@ -1848,6 +1851,111 @@ public function testWithTransactionExample(): void
18481851
// phpcs:enable
18491852
}
18501853

1854+
/**
1855+
* Queryable encryption examples (not parsed for server manual includes).
1856+
*
1857+
* @see https://jira.mongodb.org/browse/PHPLIB-863
1858+
* @see ClientSideEncryptionSpecTest::testExplicitEncryption
1859+
*/
1860+
public function testQueryableEncryption(): void
1861+
{
1862+
if ($this->isStandalone() || ($this->isShardedCluster() && ! $this->isShardedClusterUsingReplicasets())) {
1863+
$this->markTestSkipped('Queryable encryption requires replica sets');
1864+
}
1865+
1866+
if (version_compare($this->getServerVersion(), '6.0.0', '<')) {
1867+
$this->markTestSkipped('Queryable encryption requires MongoDB 6.0 or later');
1868+
}
1869+
1870+
if (! $this->isEnterprise()) {
1871+
$this->markTestSkipped('Automatic encryption requires MongoDB Enterprise');
1872+
}
1873+
1874+
// Fetch names for the database and collection under test
1875+
$collectionName = $this->getCollectionName();
1876+
$databaseName = $this->getDatabaseName();
1877+
$namespace = $this->getNamespace();
1878+
1879+
/* Create a client without auto encryption. Drop existing data in both
1880+
* the keyvault and database under test. The latter is necessary since
1881+
* setUp() only drops the collection under test, which will leave behind
1882+
* internal collections for queryable encryption. */
1883+
$client = static::createTestClient();
1884+
$client->selectDatabase('keyvault')->drop(['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]);
1885+
$client->selectDatabase($databaseName)->drop(['writeConcern' => new WriteConcern(WriteConcern::MAJORITY)]);
1886+
1887+
/* Although ClientEncryption can be constructed directly, the library
1888+
* provides a helper to do so. With this method, the keyVaultClient will
1889+
* default to the same client. */
1890+
$clientEncryption = $client->createClientEncryption([
1891+
'keyVaultNamespace' => 'keyvault.datakeys',
1892+
'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(ClientSideEncryptionSpecTest::LOCAL_MASTERKEY), 0)]],
1893+
]);
1894+
1895+
// Create two data keys, one for each encrypted field
1896+
$dataKeyId1 = $clientEncryption->createDataKey('local');
1897+
$dataKeyId2 = $clientEncryption->createDataKey('local');
1898+
1899+
$autoEncryptionOpts = [
1900+
'keyVaultNamespace' => 'keyvault.datakeys',
1901+
'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(ClientSideEncryptionSpecTest::LOCAL_MASTERKEY), 0)]],
1902+
'encryptedFieldsMap' => [
1903+
$namespace => [
1904+
'fields' => [
1905+
[
1906+
'path' => 'encryptedIndexed',
1907+
'bsonType' => 'string',
1908+
'keyId' => $dataKeyId1,
1909+
'queries' => ['queryType' => 'equality'],
1910+
],
1911+
[
1912+
'path' => 'encryptedUnindexed',
1913+
'bsonType' => 'string',
1914+
'keyId' => $dataKeyId2,
1915+
],
1916+
],
1917+
],
1918+
],
1919+
];
1920+
1921+
$encryptedClient = static::createTestClient(null, [], ['autoEncryption' => $autoEncryptionOpts]);
1922+
1923+
/* Create the collection under test. The createCollection() helper will
1924+
* reference the client's encryptedFieldsMap and create additional,
1925+
* internal collections automatically. */
1926+
$encryptedClient->selectDatabase($databaseName)->createCollection($collectionName);
1927+
$encryptedCollection = $encryptedClient->selectCollection($databaseName, $collectionName);
1928+
1929+
/* Using a client with auto encryption, insert a document with encrypted
1930+
* fields and assert that those fields are automatically decrypted when
1931+
* querying. */
1932+
$indexedValue = 'indexedValue';
1933+
$unindexedValue = 'unindexedValue';
1934+
1935+
$encryptedCollection->insertOne([
1936+
'_id' => 1,
1937+
'encryptedIndexed' => $indexedValue,
1938+
'encryptedUnindexed' => $unindexedValue,
1939+
]);
1940+
1941+
$result = $encryptedCollection->findOne(['encryptedIndexed' => $indexedValue]);
1942+
1943+
$this->assertSame(1, $result['_id']);
1944+
$this->assertSame($indexedValue, $result['encryptedIndexed']);
1945+
$this->assertSame($unindexedValue, $result['encryptedUnindexed']);
1946+
1947+
/* Using a client without auto encryption, query for the same
1948+
* document and assert that encrypted data is returned. */
1949+
$unencryptedClient = static::createTestClient();
1950+
$unencryptedCollection = $unencryptedClient->selectCollection($databaseName, $collectionName);
1951+
1952+
$result = $unencryptedCollection->findOne(['_id' => 1]);
1953+
1954+
$this->assertSame(1, $result['_id']);
1955+
$this->assertInstanceOf(Binary::class, $result['encryptedIndexed']);
1956+
$this->assertInstanceOf(Binary::class, $result['encryptedUnindexed']);
1957+
}
1958+
18511959
/**
18521960
* Return the test collection name.
18531961
*

tests/FunctionalTestCase.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use function filter_var;
2929
use function getenv;
3030
use function implode;
31+
use function in_array;
3132
use function is_array;
3233
use function is_callable;
3334
use function is_object;
@@ -376,6 +377,20 @@ protected function getServerStorageEngine(?ReadPreference $readPreference = null
376377
throw new UnexpectedValueException('Could not determine server storage engine');
377378
}
378379

380+
protected function isEnterprise(): bool
381+
{
382+
$buildInfo = $this->getPrimaryServer()->executeCommand(
383+
$this->getDatabaseName(),
384+
new Command(['buildInfo' => 1])
385+
)->toArray()[0];
386+
387+
if (isset($buildInfo->modules) && is_array($buildInfo->modules)) {
388+
return in_array('enterprise', $buildInfo->modules);
389+
}
390+
391+
throw new UnexpectedValueException('Could not determine server modules');
392+
}
393+
379394
protected function isLoadBalanced()
380395
{
381396
return $this->getPrimaryServer()->getType() == Server::TYPE_LOAD_BALANCER;

0 commit comments

Comments
 (0)