Files
mongo/jstests/fle2/libs/encrypted_client_util.js
2022-05-10 18:29:25 +00:00

381 lines
14 KiB
JavaScript

load("jstests/concurrency/fsm_workload_helpers/server_types.js"); // For isMongos.
/**
* Create a FLE client that has an unencrypted and encrypted client to the same database
*/
const kSafeContentField = "__safeContent__";
class EncryptedClient {
/**
* Create a new encrypted FLE connection to the target server with a local KMS
*
* @param {Mongo} conn Connection to mongod or mongos
* @param {string} dbName Name of database to setup key vault in
*/
constructor(conn, dbName) {
// Detect if jstests/libs/override_methods/implicitly_shard_accessed_collections.js is in
// use
this.useImplicitSharding = !(typeof (ImplicitlyShardAccessCollSettings) === "undefined");
const localKMS = {
key: BinData(
0,
"/tu9jUCBqZdwCelwE/EAm/4WqdxrSMi04B8e9uAV+m30rI1J2nhKZZtQjdvsSCwuI4erR6IEcEK+5eGUAODv43NDNIR9QheT2edWFewUfHKsl9cnzTc86meIzOmYl6dr")
};
const clientSideFLEOptions = {
kmsProviders: {
local: localKMS,
},
keyVaultNamespace: dbName + ".keystore",
schemaMap: {},
};
let connectionString = conn.host.toString();
var shell = Mongo(connectionString, clientSideFLEOptions);
var edb = shell.getDB(dbName);
var keyVault = shell.getKeyVault();
this._db = conn.getDB(dbName);
this._edb = edb;
this._keyVault = keyVault;
}
/**
* Return an encrypted database
*
* @returns Database
*/
getDB() {
return this._edb;
}
/**
* Return an unencrypted database
*
* @returns Database
*/
getRawDB() {
return this._db;
}
/**
* @returns KeyVault
*/
getKeyVault() {
return this._keyVault;
}
/**
* Create an encrypted collection. If key ids are not specified, it creates them automatically
* in the key vault.
*
* @param {string} name Name of collection
* @param {Object} options Create Collection options
*/
createEncryptionCollection(name, options) {
assert(options != undefined);
assert(options.hasOwnProperty("encryptedFields"));
assert(options.encryptedFields.hasOwnProperty("fields"));
for (let field of options.encryptedFields.fields) {
if (!field.hasOwnProperty("keyId")) {
let testkeyId = this._keyVault.createKey("local", "ignored");
field["keyId"] = testkeyId;
}
}
assert.neq(options,
undefined,
`createEncryptedCollection expected an options object, it is undefined`);
assert(
options.hasOwnProperty("encryptedFields") && typeof options.encryptedFields == "object",
`options must contain an encryptedFields document'`);
const res = assert.commandWorked(this._edb.createCollection(name, options));
const cis = this._edb.getCollectionInfos({"name": name});
assert.eq(cis.length, 1, `Expected to find one collection named '${name}'`);
const ci = cis[0];
assert(ci.hasOwnProperty("options"), `Expected collection '${name}' to have 'options'`);
const storedOptions = ci.options;
assert(options.hasOwnProperty("encryptedFields"),
`Expected collection '${name}' to have 'encryptedFields'`);
const ef = storedOptions.encryptedFields;
// All our tests use "last" as the key to query on so shard on "last" instead of "_id"
if (this.useImplicitSharding) {
let resShard = this._db.adminCommand({enableSharding: this._db.getName()});
// enableSharding may only be called once for a database.
if (resShard.code !== ErrorCodes.AlreadyInitialized) {
assert.commandWorked(
resShard, "enabling sharding on the '" + this._db.getName() + "' db failed");
}
let shardCollCmd = {
shardCollection: this._db.getName() + "." + name,
key: {last: "hashed"},
collation: {locale: "simple"}
};
resShard = this._db.adminCommand(shardCollCmd);
jsTestLog("Sharding: " + tojson(shardCollCmd));
}
assert.commandWorked(this._edb.getCollection(name).createIndex({__safeContent__: 1}));
assert.commandWorked(this._edb.createCollection(ef.escCollection));
assert.commandWorked(this._edb.createCollection(ef.eccCollection));
assert.commandWorked(this._edb.createCollection(ef.ecocCollection));
return res;
}
/**
* Assert the number of documents in the EDC and state collections is correct.
*
* @param {object} collection Collection object for EDC
* @param {number} edc Number of documents in EDC
* @param {number} esc Number of documents in ESC
* @param {number} ecc Number of documents in ECC
* @param {number} ecoc Number of documents in ECOC
*/
assertEncryptedCollectionCountsByObject(
sessionDB, name, expectedEdc, expectedEsc, expectedEcc, expectedEcoc) {
const cis = this._db.getCollectionInfos({"name": name});
assert.eq(cis.length, 1, `Expected to find one collection named '${name}'`);
const ci = cis[0];
assert(ci.hasOwnProperty("options"), `Expected collection '${name}' to have 'options'`);
const options = ci.options;
assert(options.hasOwnProperty("encryptedFields"),
`Expected collection '${name}' to have 'encryptedFields'`);
const ef = options.encryptedFields;
const actualEdc = sessionDB.getCollection(name).countDocuments({});
assert.eq(actualEdc,
expectedEdc,
`EDC document count is wrong: Actual ${actualEdc} vs Expected ${expectedEdc}`);
const actualEsc = sessionDB.getCollection(ef.escCollection).countDocuments({});
assert.eq(actualEsc,
expectedEsc,
`ESC document count is wrong: Actual ${actualEsc} vs Expected ${expectedEsc}`);
const actualEcc = sessionDB.getCollection(ef.eccCollection).countDocuments({});
assert.eq(actualEcc,
expectedEcc,
`ECC document count is wrong: Actual ${actualEcc} vs Expected ${expectedEcc}`);
const actualEcoc = sessionDB.getCollection(ef.ecocCollection).countDocuments({});
assert.eq(actualEcoc,
expectedEcoc,
`ECOC document count is wrong: Actual ${actualEcoc} vs Expected ${expectedEcoc}`);
}
/**
* Assert the number of documents in the EDC and state collections is correct.
*
* @param {string} name Name of EDC
* @param {number} edc Number of documents in EDC
* @param {number} esc Number of documents in ESC
* @param {number} ecc Number of documents in ECC
* @param {number} ecoc Number of documents in ECOC
*/
assertEncryptedCollectionCounts(name, expectedEdc, expectedEsc, expectedEcc, expectedEcoc) {
this.assertEncryptedCollectionCountsByObject(
this._db, name, expectedEdc, expectedEsc, expectedEcc, expectedEcoc);
}
/**
* Get a single document from the collection with the specified query. Ensure it contains the
specified fields when decrypted and that does fields are encrypted.
* @param {string} coll
* @param {object} query
* @param {object} fields
*/
assertOneEncryptedDocumentFields(coll, query, fields) {
let encryptedDocs = this._db.getCollection(coll).find(query).toArray();
assert.eq(encryptedDocs.length,
1,
`Expected query ${tojson(query)} to only return one document. Found ${
encryptedDocs.length}`);
let unEncryptedDocs = this._edb.getCollection(coll).find(query).toArray();
assert.eq(unEncryptedDocs.length, 1);
let encryptedDoc = encryptedDocs[0];
let unEncryptedDoc = unEncryptedDocs[0];
assert(encryptedDoc[kSafeContentField] !== undefined);
for (let field in fields) {
assert(encryptedDoc.hasOwnProperty(field),
`Could not find ${field} in encrypted ${tojson(encryptedDoc)}`);
assert(unEncryptedDoc.hasOwnProperty(field),
`Could not find ${field} in unEncrypted ${tojson(unEncryptedDoc)}`);
let rawField = encryptedDoc[field];
assertIsIndexedEncryptedField(rawField);
let unEncryptedField = unEncryptedDoc[field];
assert.eq(unEncryptedField, fields[field]);
}
}
assertWriteCommandReplyFields(response) {
if (isMongod(this._edb)) {
// These fields are replica set specific
assert(response.hasOwnProperty("electionId"));
assert(response.hasOwnProperty("opTime"));
}
assert(response.hasOwnProperty("$clusterTime"));
assert(response.hasOwnProperty("operationTime"));
}
/**
* Take a snapshot of a collection sorted by _id, run a operation, take a second snapshot.
*
* Ensure that the documents listed by index in unchangedDocumentIndexArray remain unchanged.
* Ensure that the documents listed by index in changedDocumentIndexArray are changed.
*
* @param {string} collName
* @param {Array} unchangedDocumentIndexArray
* @param {Array} changedDocumentIndexArray
* @param {Function} func
* @returns
*/
assertDocumentChanges(collName, unchangedDocumentIndexArray, changedDocumentIndexArray, func) {
let coll = this._edb.getCollection(collName);
let beforeDocuments = coll.find({}).sort({_id: 1}).toArray();
let x = func();
let afterDocuments = coll.find({}).sort({_id: 1}).toArray();
for (let unchangedDocumentIndex of unchangedDocumentIndexArray) {
assert.eq(beforeDocuments[unchangedDocumentIndex],
afterDocuments[unchangedDocumentIndex],
"Expected document index '" + unchangedDocumentIndex + "' to be the same." +
tojson(beforeDocuments[unchangedDocumentIndex]) + "\n==========\n" +
tojson(afterDocuments[unchangedDocumentIndex]));
}
for (let changedDocumentIndex of changedDocumentIndexArray) {
assert.neq(
beforeDocuments[changedDocumentIndex],
afterDocuments[changedDocumentIndex],
"Expected document index '" + changedDocumentIndex +
"' to be different. == " + tojson(beforeDocuments[changedDocumentIndex]) +
"\n==========\n" + tojson(afterDocuments[changedDocumentIndex]));
}
return x;
}
/**
* Verify that the collection 'collName' contains exactly the documents 'docs'.
*
* @param {string} collName
* @param {Array} docs
* @returns
*/
assertEncryptedCollectionDocuments(collName, docs) {
let coll = this._edb.getCollection(collName);
let onDiskDocs = coll.find({}, {[kSafeContentField]: 0}).sort({_id: 1}).toArray();
assert.docEq(onDiskDocs, docs);
}
assertStateCollectionsAfterCompact(collName, ecocExists) {
const baseCollInfos = this._edb.getCollectionInfos({"name": collName});
assert.eq(baseCollInfos.length, 1);
const baseCollInfo = baseCollInfos[0];
assert(baseCollInfo.options.encryptedFields !== undefined);
const checkMap = {};
// Always expect ESC and ECC collections, optionally expect ECOC.
// ECOC is not expected in sharded clusters.
checkMap[baseCollInfo.options.encryptedFields.escCollection] = true;
checkMap[baseCollInfo.options.encryptedFields.eccCollection] = true;
checkMap[baseCollInfo.options.encryptedFields.ecocCollection] = ecocExists;
checkMap[baseCollInfo.options.encryptedFields.ecocCollection + ".compact"] = false;
const edb = this._edb;
Object.keys(checkMap).forEach(function(coll) {
const info = edb.getCollectionInfos({"name": coll});
const msg = coll + (checkMap[coll] ? " does not exist" : " exists") + " after compact";
assert.eq(info.length, checkMap[coll], msg);
});
}
}
function runEncryptedTest(db, dbName, collName, encryptedFields, runTestsCallback) {
const dbTest = db.getSiblingDB(dbName);
dbTest.dropDatabase();
// Delete existing keyIds from encryptedFields to force
// EncryptedClient to generate new keys on the new DB.
for (let field of encryptedFields.fields) {
if (field.hasOwnProperty("keyId")) {
delete field.keyId;
}
}
let client = new EncryptedClient(db.getMongo(), dbName);
assert.commandWorked(
client.createEncryptionCollection(collName, {encryptedFields: encryptedFields}));
let edb = client.getDB();
runTestsCallback(edb, client);
}
/**
* @returns Returns true if talking to a sharded cluster
*/
function isFLE2ShardingEnabled() {
return typeof (testingFLESharding) == "undefined" || testingFLESharding === true;
}
/**
* @returns Returns true if talking to a replica set
*/
function isFLE2ReplicationEnabled() {
return typeof (testingReplication) == "undefined" || testingReplication === true;
}
/**
* Assert a field is an indexed encrypted field
*
* @param {BinData} value bindata value
*/
function assertIsIndexedEncryptedField(value) {
assert(value instanceof BinData, "Expected BinData, found: " + value);
assert.eq(value.subtype(), 6, "Expected Encrypted bindata: " + value);
assert(value.hex().startsWith("07"),
"Expected subtype 7 but found the wrong type: " + value.hex());
}
/**
* Assert a field is an unindexed encrypted field
*
* @param {BinData} value bindata value
*/
function assertIsUnindexedEncryptedField(value) {
assert(value instanceof BinData, "Expected BinData, found: " + value);
assert.eq(value.subtype(), 6, "Expected Encrypted bindata: " + value);
assert(value.hex().startsWith("06"),
"Expected subtype 6 but found the wrong type: " + value.hex());
}