PHPLIB-510: Implement FLE spec tests

parent c9b14ee3
...@@ -26,9 +26,15 @@ use function is_array; ...@@ -26,9 +26,15 @@ use function is_array;
use function is_object; use function is_object;
use function is_string; use function is_string;
use function key; use function key;
use function ob_get_clean;
use function ob_start;
use function parse_url; use function parse_url;
use function phpinfo;
use function preg_match; use function preg_match;
use function preg_quote;
use function sprintf;
use function version_compare; use function version_compare;
use const INFO_MODULES;
abstract class FunctionalTestCase extends TestCase abstract class FunctionalTestCase extends TestCase
{ {
...@@ -375,6 +381,17 @@ abstract class FunctionalTestCase extends TestCase ...@@ -375,6 +381,17 @@ abstract class FunctionalTestCase extends TestCase
} }
} }
protected function skipIfClientSideEncryptionIsNotSupported()
{
if (version_compare($this->getFeatureCompatibilityVersion(), '4.2', '<')) {
$this->markTestSkipped('Client Side Encryption only supported on FCV 4.2 or higher');
}
if ($this->getModuleInfo('libmongocrypt') === 'disabled') {
$this->markTestSkipped('Client Side Encryption is not enabled in the MongoDB extension');
}
}
protected function skipIfTransactionsAreNotSupported() protected function skipIfTransactionsAreNotSupported()
{ {
if ($this->getPrimaryServer()->getType() === Server::TYPE_STANDALONE) { if ($this->getPrimaryServer()->getType() === Server::TYPE_STANDALONE) {
...@@ -420,6 +437,26 @@ abstract class FunctionalTestCase extends TestCase ...@@ -420,6 +437,26 @@ abstract class FunctionalTestCase extends TestCase
} }
} }
/**
* @param string $row
*
* @return string|null
*/
private function getModuleInfo($row)
{
ob_start();
phpinfo(INFO_MODULES);
$info = ob_get_clean();
$pattern = sprintf('/^%s([\w ]+)$/m', preg_quote($row . ' => '));
if (preg_match($pattern, $info, $matches) !== 1) {
return null;
}
return $matches[1];
}
/** /**
* Checks if the failCommand command is supported on this server version * Checks if the failCommand command is supported on this server version
* *
......
...@@ -180,7 +180,7 @@ class ChangeStreamsSpecTest extends FunctionalTestCase ...@@ -180,7 +180,7 @@ class ChangeStreamsSpecTest extends FunctionalTestCase
switch ($test->target) { switch ($test->target) {
case 'client': case 'client':
return $context->client->watch($pipeline, $options); return $context->getClient()->watch($pipeline, $options);
case 'database': case 'database':
return $context->getDatabase()->watch($pipeline, $options); return $context->getDatabase()->watch($pipeline, $options);
case 'collection': case 'collection':
...@@ -228,7 +228,7 @@ class ChangeStreamsSpecTest extends FunctionalTestCase ...@@ -228,7 +228,7 @@ class ChangeStreamsSpecTest extends FunctionalTestCase
{ {
$context = $this->getContext(); $context = $this->getContext();
$database = $context->client->selectDatabase($databaseName); $database = $context->getClient()->selectDatabase($databaseName);
$database->drop($context->defaultWriteOptions); $database->drop($context->defaultWriteOptions);
$database->createCollection($collectionName, $context->defaultWriteOptions); $database->createCollection($collectionName, $context->defaultWriteOptions);
} }
......
This diff is collapsed.
...@@ -77,6 +77,16 @@ class CommandExpectations implements CommandSubscriber ...@@ -77,6 +77,16 @@ class CommandExpectations implements CommandSubscriber
return $o; return $o;
} }
public static function fromClientSideEncryption(array $expectedEvents)
{
$o = new self($expectedEvents);
$o->ignoreCommandFailed = true;
$o->ignoreCommandSucceeded = true;
return $o;
}
public static function fromCommandMonitoring(array $expectedEvents) public static function fromCommandMonitoring(array $expectedEvents)
{ {
return new self($expectedEvents); return new self($expectedEvents);
......
...@@ -8,11 +8,14 @@ use MongoDB\Driver\ReadConcern; ...@@ -8,11 +8,14 @@ use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference; use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\Session; use MongoDB\Driver\Session;
use MongoDB\Driver\WriteConcern; use MongoDB\Driver\WriteConcern;
use PHPUnit\Framework\SkippedTestError;
use stdClass; use stdClass;
use function array_diff_key; use function array_diff_key;
use function array_keys; use function array_keys;
use function getenv;
use function implode; use function implode;
use function mt_rand; use function mt_rand;
use function uniqid;
/** /**
* Execution context for spec tests. * Execution context for spec tests.
...@@ -26,7 +29,7 @@ final class Context ...@@ -26,7 +29,7 @@ final class Context
public $bucketName; public $bucketName;
/** @var Client|null */ /** @var Client|null */
public $client; private $client;
/** @var string */ /** @var string */
public $collectionName; public $collectionName;
...@@ -55,6 +58,12 @@ final class Context ...@@ -55,6 +58,12 @@ final class Context
/** @var object */ /** @var object */
public $session1Lsid; public $session1Lsid;
/** @var Client|null */
private $encryptedClient;
/** @var bool */
private $useEncryptedClient = false;
/** /**
* @param string $databaseName * @param string $databaseName
* @param string $collectionName * @param string $collectionName
...@@ -66,6 +75,20 @@ final class Context ...@@ -66,6 +75,20 @@ final class Context
$this->outcomeCollectionName = $collectionName; $this->outcomeCollectionName = $collectionName;
} }
public function disableEncryption()
{
$this->useEncryptedClient = false;
}
public function enableEncryption()
{
if (! $this->encryptedClient instanceof Client) {
throw new LogicException('Cannot enable encryption without autoEncryption options');
}
$this->useEncryptedClient = true;
}
public static function fromChangeStreams(stdClass $test, $databaseName, $collectionName) public static function fromChangeStreams(stdClass $test, $databaseName, $collectionName)
{ {
$o = new self($databaseName, $collectionName); $o = new self($databaseName, $collectionName);
...@@ -75,6 +98,41 @@ final class Context ...@@ -75,6 +98,41 @@ final class Context
return $o; return $o;
} }
public static function fromClientSideEncryption(stdClass $test, $databaseName, $collectionName)
{
$o = new self($databaseName, $collectionName);
$clientOptions = isset($test->clientOptions) ? (array) $test->clientOptions : [];
/* mongocryptd caches collection information, which causes test failures
* if we reuse the client. Thus, we add a random value to ensure we're
* creating a new client for each test. */
$driverOptions = ['random' => uniqid()];
$autoEncryptionOptions = [];
if (isset($clientOptions['autoEncryptOpts'])) {
$autoEncryptionOptions = (array) $clientOptions['autoEncryptOpts'] + ['keyVaultNamespace' => 'admin.datakeys'];
unset($clientOptions['autoEncryptOpts']);
if (isset($autoEncryptionOptions['kmsProviders']->aws)) {
$autoEncryptionOptions['kmsProviders']->aws = self::getAWSCredentials();
}
}
if (isset($test->outcome->collection->name)) {
$o->outcomeCollectionName = $test->outcome->collection->name;
}
$o->client = new Client(FunctionalTestCase::getUri(), $clientOptions, $driverOptions);
if ($autoEncryptionOptions !== []) {
$o->encryptedClient = new Client(FunctionalTestCase::getUri(), $clientOptions, $driverOptions + ['autoEncryption' => $autoEncryptionOptions]);
}
return $o;
}
public static function fromCommandMonitoring(stdClass $test, $databaseName, $collectionName) public static function fromCommandMonitoring(stdClass $test, $databaseName, $collectionName)
{ {
$o = new self($databaseName, $collectionName); $o = new self($databaseName, $collectionName);
...@@ -173,12 +231,29 @@ final class Context ...@@ -173,12 +231,29 @@ final class Context
return $o; return $o;
} }
/**
* @return array
*
* @throws SkippedTestError
*/
public static function getAWSCredentials()
{
if (! getenv('AWS_ACCESS_KEY_ID') || ! getenv('AWS_SECRET_ACCESS_KEY')) {
throw new SkippedTestError('Please configure AWS credentials to use AWS KMS provider.');
}
return [
'accessKeyId' => getenv('AWS_ACCESS_KEY_ID'),
'secretAccessKey' => getenv('AWS_SECRET_ACCESS_KEY'),
];
}
/** /**
* @return Client * @return Client
*/ */
public function getClient() public function getClient()
{ {
return $this->client; return $this->useEncryptedClient && $this->encryptedClient ? $this->encryptedClient : $this->client;
} }
public function getCollection(array $collectionOptions = []) public function getCollection(array $collectionOptions = [])
...@@ -310,7 +385,7 @@ final class Context ...@@ -310,7 +385,7 @@ final class Context
public function selectCollection($databaseName, $collectionName, array $collectionOptions = []) public function selectCollection($databaseName, $collectionName, array $collectionOptions = [])
{ {
return $this->client->selectCollection( return $this->getClient()->selectCollection(
$databaseName, $databaseName,
$collectionName, $collectionName,
$this->prepareOptions($collectionOptions) $this->prepareOptions($collectionOptions)
...@@ -319,7 +394,7 @@ final class Context ...@@ -319,7 +394,7 @@ final class Context
public function selectDatabase($databaseName, array $databaseOptions = []) public function selectDatabase($databaseName, array $databaseOptions = [])
{ {
return $this->client->selectDatabase( return $this->getClient()->selectDatabase(
$databaseName, $databaseName,
$this->prepareOptions($databaseOptions) $this->prepareOptions($databaseOptions)
); );
......
...@@ -75,6 +75,11 @@ final class ErrorExpectation ...@@ -75,6 +75,11 @@ final class ErrorExpectation
return $o; return $o;
} }
public static function fromClientSideEncryption(stdClass $operation)
{
return self::fromGenericOperation($operation);
}
public static function fromCrud(stdClass $result) public static function fromCrud(stdClass $result)
{ {
$o = new self(); $o = new self();
......
...@@ -220,6 +220,7 @@ class FunctionalTestCase extends BaseFunctionalTestCase ...@@ -220,6 +220,7 @@ class FunctionalTestCase extends BaseFunctionalTestCase
$context = $this->getContext(); $context = $this->getContext();
$collection = $collectionName ? $context->selectCollection($context->databaseName, $collectionName) : $context->getCollection(); $collection = $collectionName ? $context->selectCollection($context->databaseName, $collectionName) : $context->getCollection();
$collection->insertMany($documents, $context->defaultWriteOptions); $collection->insertMany($documents, $context->defaultWriteOptions);
return; return;
......
...@@ -116,6 +116,20 @@ final class Operation ...@@ -116,6 +116,20 @@ final class Operation
return $o; return $o;
} }
public static function fromClientSideEncryption(stdClass $operation)
{
$o = new self($operation);
$o->errorExpectation = ErrorExpectation::fromClientSideEncryption($operation);
$o->resultExpectation = ResultExpectation::fromClientSideEncryption($operation, $o->getResultAssertionType());
if (isset($operation->collectionOptions)) {
$o->collectionOptions = (array) $operation->collectionOptions;
}
return $o;
}
public static function fromCommandMonitoring(stdClass $operation) public static function fromCommandMonitoring(stdClass $operation)
{ {
$o = new self($operation); $o = new self($operation);
......
...@@ -85,6 +85,19 @@ final class ResultExpectation ...@@ -85,6 +85,19 @@ final class ResultExpectation
return $o; return $o;
} }
public static function fromClientSideEncryption(stdClass $operation, $defaultAssertionType)
{
if (property_exists($operation, 'result') && ! self::isErrorResult($operation->result)) {
$assertionType = $operation->result === null ? self::ASSERT_NULL : $defaultAssertionType;
$expectedValue = $operation->result;
} else {
$assertionType = self::ASSERT_NOTHING;
$expectedValue = null;
}
return new self($assertionType, $expectedValue);
}
public static function fromCrud(stdClass $operation, $defaultAssertionType) public static function fromCrud(stdClass $operation, $defaultAssertionType)
{ {
if (property_exists($operation, 'result') && ! self::isErrorResult($operation->result)) { if (property_exists($operation, 'result') && ! self::isErrorResult($operation->result)) {
......
{
"status": {
"$numberInt": "1"
},
"_id": {
"$binary": {
"base64": "AWSAAAAAAAAAAAAAAAAAAA==",
"subType": "04"
}
},
"masterKey": {
"region": "us-east-1",
"key": "arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0",
"provider": "aws"
},
"updateDate": {
"$date": {
"$numberLong": "1557827033449"
}
},
"keyMaterial": {
"$binary": {
"base64": "AQICAHhQNmWG2CzOm1dq3kWLM+iDUZhEqnhJwH9wZVpuZ94A8gEqnsxXlR51T5EbEVezUqqKAAAAwjCBvwYJKoZIhvcNAQcGoIGxMIGuAgEAMIGoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHa4jo6yp0Z18KgbUgIBEIB74sKxWtV8/YHje5lv5THTl0HIbhSwM6EqRlmBiFFatmEWaeMk4tO4xBX65eq670I5TWPSLMzpp8ncGHMmvHqRajNBnmFtbYxN3E3/WjxmdbOOe+OXpnGJPcGsftc7cB2shRfA4lICPnE26+oVNXT6p0Lo20nY5XC7jyCO",
"subType": "00"
}
},
"creationDate": {
"$date": {
"$numberLong": "1557827033449"
}
},
"keyAltNames": ["aws"]
}
\ No newline at end of file
{
"status": {
"$numberInt": "1"
},
"_id": {
"$binary": {
"base64": "LOCALAAAAAAAAAAAAAAAAA==",
"subType": "04"
}
},
"masterKey": {
"provider": "local"
},
"updateDate": {
"$date": {
"$numberLong": "1557827033449"
}
},
"keyMaterial": {
"$binary": {
"base64": "Ce9HSz/HKKGkIt4uyy+jDuKGA+rLC2cycykMo6vc8jXxqa1UVDYHWq1r+vZKbnnSRBfB981akzRKZCFpC05CTyFqDhXv6OnMjpG97OZEREGIsHEYiJkBW0jJJvfLLgeLsEpBzsro9FztGGXASxyxFRZFhXvHxyiLOKrdWfs7X1O/iK3pEoHMx6uSNSfUOgbebLfIqW7TO++iQS5g1xovXA==",
"subType": "00"
}
},
"creationDate": {
"$date": {
"$numberLong": "1557827033449"
}
},
"keyAltNames": [ "local" ]
}
\ No newline at end of file
This diff is collapsed.
{
"status": {
"$numberInt": "1"
},
"_id": {
"$binary": {
"base64": "LOCALAAAAAAAAAAAAAAAAA==",
"subType": "04"
}
},
"masterKey": {
"provider": "local"
},
"updateDate": {
"$date": {
"$numberLong": "1557827033449"
}
},
"keyMaterial": {
"$binary": {
"base64": "Ce9HSz/HKKGkIt4uyy+jDuKGA+rLC2cycykMo6vc8jXxqa1UVDYHWq1r+vZKbnnSRBfB981akzRKZCFpC05CTyFqDhXv6OnMjpG97OZEREGIsHEYiJkBW0jJJvfLLgeLsEpBzsro9FztGGXASxyxFRZFhXvHxyiLOKrdWfs7X1O/iK3pEoHMx6uSNSfUOgbebLfIqW7TO++iQS5g1xovXA==",
"subType": "00"
}
},
"creationDate": {
"$date": {
"$numberLong": "1557827033449"
}
},
"keyAltNames": [ "local" ]
}
\ No newline at end of file
{
"properties": {
"encrypted": {
"encrypt": {
"keyId": [
{
"$binary": {
"base64": "LOCALAAAAAAAAAAAAAAAAA==",
"subType": "04"
}
}
],
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
}
}
},
"bsonType": "object"
}
{
"00": "a",
"01": "a",
"02": "a",
"03": "a",
"04": "a",
"05": "a",
"06": "a",
"07": "a",
"08": "a",
"09": "a",
"10": "a",
"11": "a",
"12": "a",
"13": "a",
"14": "a",
"15": "a",
"16": "a",
"17": "a",
"18": "a",
"19": "a",
"20": "a",
"21": "a",
"22": "a",
"23": "a",
"24": "a",
"25": "a",
"26": "a",
"27": "a",
"28": "a",
"29": "a",
"30": "a",
"31": "a",
"32": "a",
"33": "a",
"34": "a",
"35": "a",
"36": "a",
"37": "a",
"38": "a",
"39": "a",
"40": "a",
"41": "a",
"42": "a",
"43": "a",
"44": "a",
"45": "a",
"46": "a",
"47": "a",
"48": "a",
"49": "a",
"50": "a",
"51": "a",
"52": "a",
"53": "a",
"54": "a",
"55": "a",
"56": "a",
"57": "a",
"58": "a",
"59": "a",
"60": "a",
"61": "a",
"62": "a",
"63": "a",
"64": "a",
"65": "a",
"66": "a",
"67": "a",
"68": "a",
"69": "a",
"70": "a",
"71": "a",
"72": "a",
"73": "a",
"74": "a",
"75": "a",
"76": "a",
"77": "a",
"78": "a",
"79": "a",
"80": "a",
"81": "a",
"82": "a",
"83": "a",
"84": "a",
"85": "a",
"86": "a",
"87": "a",
"88": "a",
"89": "a",
"90": "a",
"91": "a",
"92": "a",
"93": "a",
"94": "a",
"95": "a",
"96": "a",
"97": "a",
"98": "a",
"99": "a"
}
\ No newline at end of file
{
"status": {
"$numberInt": "1"
},
"_id": {
"$binary": {
"base64": "LOCALAAAAAAAAAAAAAAAAA==",
"subType": "04"
}
},
"masterKey": {
"provider": "local"
},
"updateDate": {
"$date": {
"$numberLong": "1557827033449"
}
},
"keyMaterial": {
"$binary": {
"base64": "Ce9HSz/HKKGkIt4uyy+jDuKGA+rLC2cycykMo6vc8jXxqa1UVDYHWq1r+vZKbnnSRBfB981akzRKZCFpC05CTyFqDhXv6OnMjpG97OZEREGIsHEYiJkBW0jJJvfLLgeLsEpBzsro9FztGGXASxyxFRZFhXvHxyiLOKrdWfs7X1O/iK3pEoHMx6uSNSfUOgbebLfIqW7TO++iQS5g1xovXA==",
"subType": "00"
}
},
"creationDate": {
"$date": {
"$numberLong": "1557827033449"
}
},
"keyAltNames": [ "local" ]
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{
"runOn": [
{
"minServerVersion": "4.1.10"
}
],
"database_name": "default",
"collection_name": "default",
"data": [],
"json_schema": {},
"key_vault_data": [
{
"status": 1,
"_id": {
"$binary": {
"base64": "AAAAAAAAAAAAAAAAAAAAAA==",
"subType": "04"
}
},
"masterKey": {
"provider": "aws",
"key": "arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0",
"region": "us-east-1"
},
"updateDate": {
"$date": {
"$numberLong": "1552949630483"
}
},
"keyMaterial": {
"$binary": {
"base64": "AQICAHhQNmWG2CzOm1dq3kWLM+iDUZhEqnhJwH9wZVpuZ94A8gEqnsxXlR51T5EbEVezUqqKAAAAwjCBvwYJKoZIhvcNAQcGoIGxMIGuAgEAMIGoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHa4jo6yp0Z18KgbUgIBEIB74sKxWtV8/YHje5lv5THTl0HIbhSwM6EqRlmBiFFatmEWaeMk4tO4xBX65eq670I5TWPSLMzpp8ncGHMmvHqRajNBnmFtbYxN3E3/WjxmdbOOe+OXpnGJPcGsftc7cB2shRfA4lICPnE26+oVNXT6p0Lo20nY5XC7jyCO",
"subType": "00"
}
},
"creationDate": {
"$date": {
"$numberLong": "1552949630483"
}
},
"keyAltNames": [
"altname",
"another_altname"
]
}
],
"tests": [
{
"description": "ping is bypassed",
"clientOptions": {
"autoEncryptOpts": {
"kmsProviders": {
"aws": {}
}
}
},
"operations": [
{
"name": "runCommand",
"object": "database",
"command_name": "ping",
"arguments": {
"command": {
"ping": 1
}
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"ping": 1
},
"command_name": "ping"
}
}
]
},
{
"description": "current op is not bypassed",
"clientOptions": {
"autoEncryptOpts": {
"kmsProviders": {
"aws": {}
}
}
},
"operations": [
{
"name": "runCommand",
"object": "database",
"command_name": "currentOp",
"arguments": {
"command": {
"currentOp": 1
}
},
"result": {
"errorContains": "command not supported for auto encryption: currentOp"
}
}
]
}
]
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{
"runOn": [
{
"maxServerVersion": "4.0"
}
],
"database_name": "default",
"collection_name": "default",
"data": [],
"key_vault_data": [
{
"status": 1,
"_id": {
"$binary": {
"base64": "AAAAAAAAAAAAAAAAAAAAAA==",
"subType": "04"
}
},
"masterKey": {
"provider": "aws",
"key": "arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0",
"region": "us-east-1"
},
"updateDate": {
"$date": {
"$numberLong": "1552949630483"
}
},
"keyMaterial": {
"$binary": {
"base64": "AQICAHhQNmWG2CzOm1dq3kWLM+iDUZhEqnhJwH9wZVpuZ94A8gEqnsxXlR51T5EbEVezUqqKAAAAwjCBvwYJKoZIhvcNAQcGoIGxMIGuAgEAMIGoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHa4jo6yp0Z18KgbUgIBEIB74sKxWtV8/YHje5lv5THTl0HIbhSwM6EqRlmBiFFatmEWaeMk4tO4xBX65eq670I5TWPSLMzpp8ncGHMmvHqRajNBnmFtbYxN3E3/WjxmdbOOe+OXpnGJPcGsftc7cB2shRfA4lICPnE26+oVNXT6p0Lo20nY5XC7jyCO",
"subType": "00"
}
},
"creationDate": {
"$date": {
"$numberLong": "1552949630483"
}
},
"keyAltNames": [
"altname",
"another_altname"
]
}
],
"tests": [
{
"description": "operation fails with maxWireVersion < 8",
"clientOptions": {
"autoEncryptOpts": {
"kmsProviders": {
"aws": {}
}
}
},
"operations": [
{
"name": "insertOne",
"arguments": {
"document": {
"encrypted_string": "string0"
}
},
"result": {
"errorContains": "Auto-encryption requires a minimum MongoDB version of 4.2"
}
}
]
}
]
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment