Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-6136): parse cursor responses on demand #4112

Merged
merged 33 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c9fc786
chore: reuse parse work
nbbeeken May 9, 2024
91418d8
chore: turn back on cursorResponse
nbbeeken May 9, 2024
414cdfa
wip
nbbeeken May 9, 2024
cd29ba1
wip
nbbeeken May 10, 2024
a342412
feat(NODE-6136): parse cursor responses on demand
nbbeeken May 15, 2024
9eff48a
wip
nbbeeken May 10, 2024
4534ada
fix: FLE
nbbeeken May 17, 2024
0075729
test: needs fail points
nbbeeken May 17, 2024
79acc71
test: undo bench changes
nbbeeken May 17, 2024
dde8250
chore: cleanup
nbbeeken May 17, 2024
0b825f1
wip
nbbeeken Jun 3, 2024
e0c2250
wip
nbbeeken Jun 4, 2024
1c10a1f
chore: fix unit tests
nbbeeken Jun 4, 2024
be871b8
chore: fix wc throw location
nbbeeken Jun 4, 2024
6989a54
fix: add get overload properly
nbbeeken Jun 5, 2024
9e07ea8
chore: use serialize to make empty_v
nbbeeken Jun 6, 2024
6d081f4
docs: add comment about type crime
nbbeeken Jun 6, 2024
241a08e
cruft
nbbeeken Jun 6, 2024
de3c271
chore: type annotation
nbbeeken Jun 6, 2024
4743695
chore: move ExecutionResult and document
nbbeeken Jun 6, 2024
cba9c49
test: add match to expected errors
nbbeeken Jun 6, 2024
acbb323
test: uncomment wc error ctor tests
nbbeeken Jun 6, 2024
1ffa752
fix: super generic
nbbeeken Jun 7, 2024
fb7de90
Merge branch 'main' into NODE-6136-cursor-response
nbbeeken Jun 7, 2024
827e3f7
chore: fix nullish documents
nbbeeken Jun 7, 2024
00b90ea
fix: pass through options
nbbeeken Jun 7, 2024
e28dbbb
Merge branch 'main' into NODE-6136-cursor-response
nbbeeken Jun 7, 2024
e517ab3
refactor: move CountDocument logic into collection API (#4138)
nbbeeken Jun 10, 2024
9d75303
Revert "refactor: move CountDocument logic into collection API" (#4139)
nbbeeken Jun 10, 2024
b9cfb7c
Merge branch 'main' into NODE-6136-cursor-response
nbbeeken Jun 10, 2024
c1c8ba4
chore: only attach encryptedResponse to cursor response
nbbeeken Jun 10, 2024
d5214c8
chore: clean up TS for "required"
nbbeeken Jun 11, 2024
eb0b618
Merge branch 'main' into NODE-6136-cursor-response
baileympearson Jun 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
UUID
} from 'bson';

/** @internal */
export type BSONElement = BSON.OnDemand['BSONElement'];

export function parseToElementsToArray(bytes: Uint8Array, offset?: number): BSONElement[] {
Expand Down
79 changes: 9 additions & 70 deletions src/client-side-encryption/auto_encrypter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {

import { deserialize, type Document, serialize } from '../bson';
import { type CommandOptions, type ProxyOptions } from '../cmap/connection';
import { kDecorateResult } from '../constants';
import { getMongoDBClientEncryption } from '../deps';
import { MongoRuntimeError } from '../error';
import { MongoClient, type MongoClientOptions } from '../mongo_client';
Expand Down Expand Up @@ -212,15 +213,6 @@ export const AutoEncryptionLoggerLevel = Object.freeze({
export type AutoEncryptionLoggerLevel =
(typeof AutoEncryptionLoggerLevel)[keyof typeof AutoEncryptionLoggerLevel];

// Typescript errors if we index objects with `Symbol.for(...)`, so
// to avoid TS errors we pull them out into variables. Then we can type
// the objects (and class) that we expect to see them on and prevent TS
// errors.
/** @internal */
const kDecorateResult = Symbol.for('@@mdb.decorateDecryptionResult');
/** @internal */
const kDecoratedKeys = Symbol.for('@@mdb.decryptedKeys');

/**
* @internal An internal class to be used by the driver for auto encryption
* **NOTE**: Not meant to be instantiated directly, this is for internal use only.
Expand Down Expand Up @@ -467,16 +459,18 @@ export class AutoEncrypter {
proxyOptions: this._proxyOptions,
tlsOptions: this._tlsOptions
});
return await stateMachine.execute<Document>(this, context);

return deserialize(await stateMachine.execute(this, context), {
promoteValues: false,
promoteLongs: false
});
}

/**
* Decrypt a command response
*/
async decrypt(response: Uint8Array | Document, options: CommandOptions = {}): Promise<Document> {
const buffer = Buffer.isBuffer(response) ? response : serialize(response, options);

const context = this._mongocrypt.makeDecryptionContext(buffer);
async decrypt(response: Uint8Array, options: CommandOptions = {}): Promise<Uint8Array> {
const context = this._mongocrypt.makeDecryptionContext(response);

context.id = this._contextCounter++;

Expand All @@ -486,12 +480,7 @@ export class AutoEncrypter {
tlsOptions: this._tlsOptions
});

const decorateResult = this[kDecorateResult];
const result = await stateMachine.execute<Document>(this, context);
if (decorateResult) {
decorateDecryptionResult(result, response);
}
return result;
return await stateMachine.execute(this, context);
}

/**
Expand All @@ -518,53 +507,3 @@ export class AutoEncrypter {
return AutoEncrypter.getMongoCrypt().libmongocryptVersion;
}
}

/**
* Recurse through the (identically-shaped) `decrypted` and `original`
* objects and attach a `decryptedKeys` property on each sub-object that
* contained encrypted fields. Because we only call this on BSON responses,
* we do not need to worry about circular references.
*
* @internal
*/
function decorateDecryptionResult(
decrypted: Document & { [kDecoratedKeys]?: Array<string> },
original: Document,
isTopLevelDecorateCall = true
): void {
if (isTopLevelDecorateCall) {
// The original value could have been either a JS object or a BSON buffer
if (Buffer.isBuffer(original)) {
original = deserialize(original);
}
if (Buffer.isBuffer(decrypted)) {
throw new MongoRuntimeError('Expected result of decryption to be deserialized BSON object');
}
}

if (!decrypted || typeof decrypted !== 'object') return;
for (const k of Object.keys(decrypted)) {
const originalValue = original[k];

// An object was decrypted by libmongocrypt if and only if it was
// a BSON Binary object with subtype 6.
if (originalValue && originalValue._bsontype === 'Binary' && originalValue.sub_type === 6) {
if (!decrypted[kDecoratedKeys]) {
Object.defineProperty(decrypted, kDecoratedKeys, {
value: [],
configurable: true,
enumerable: false,
writable: false
});
}
// this is defined in the preceding if-statement
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
decrypted[kDecoratedKeys]!.push(k);
// Do not recurse into this decrypted value. It could be a sub-document/array,
// in which case there is no original value associated with its subfields.
continue;
}

decorateDecryptionResult(decrypted[k], originalValue, false);
}
}
12 changes: 6 additions & 6 deletions src/client-side-encryption/client_encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
MongoCryptOptions
} from 'mongodb-client-encryption';

import { type Binary, type Document, type Long, serialize, type UUID } from '../bson';
import { type Binary, deserialize, type Document, type Long, serialize, type UUID } from '../bson';
import { type AnyBulkWriteOperation, type BulkWriteResult } from '../bulk/common';
import { type ProxyOptions } from '../cmap/connection';
import { type Collection } from '../collection';
Expand Down Expand Up @@ -202,7 +202,7 @@ export class ClientEncryption {
tlsOptions: this._tlsOptions
});

const dataKey = await stateMachine.execute<DataKey>(this, context);
const dataKey = deserialize(await stateMachine.execute(this, context)) as DataKey;

const { db: dbName, collection: collectionName } = MongoDBCollectionNamespace.fromString(
this._keyVaultNamespace
Expand Down Expand Up @@ -259,7 +259,7 @@ export class ClientEncryption {
tlsOptions: this._tlsOptions
});

const { v: dataKeys } = await stateMachine.execute<{ v: DataKey[] }>(this, context);
const { v: dataKeys } = deserialize(await stateMachine.execute(this, context));
if (dataKeys.length === 0) {
return {};
}
Expand Down Expand Up @@ -640,7 +640,7 @@ export class ClientEncryption {
tlsOptions: this._tlsOptions
});

const { v } = await stateMachine.execute<{ v: T }>(this, context);
const { v } = deserialize(await stateMachine.execute(this, context));

return v;
}
Expand Down Expand Up @@ -719,8 +719,8 @@ export class ClientEncryption {
});
const context = this._mongoCrypt.makeExplicitEncryptionContext(valueBuffer, contextOptions);

const result = await stateMachine.execute<{ v: Binary }>(this, context);
return result.v;
const { v } = deserialize(await stateMachine.execute(this, context));
return v;
}
}

Expand Down
41 changes: 25 additions & 16 deletions src/client-side-encryption/state_machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ export type CSFLEKMSTlsOptions = {
azure?: ClientEncryptionTlsOptions;
};

/**
* This is kind of a hack. For `rewrapManyDataKey`, we have tests that
* guarantee that when there are no matching keys, `rewrapManyDataKey` returns
* nothing. We also have tests for auto encryption that guarantee for `encrypt`
* we return an error when there are no matching keys. This error is generated in
* subsequent iterations of the state machine.
* Some apis (`encrypt`) throw if there are no filter matches and others (`rewrapManyDataKey`)
* do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
* will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
* otherwise we'll return `{ v: [] }`.
*/
const EMPTY_V = Uint8Array.from([
...[13, 0, 0, 0], // document size = 13 bytes
...[
...[4, 118, 0], // array type (4), "v\x00" basic latin "v"
...[5, 0, 0, 0, 0] // empty document (5 byte size, null terminator)
],
0 // null terminator
]);
baileympearson marked this conversation as resolved.
Show resolved Hide resolved

/**
* @internal
*
Expand Down Expand Up @@ -154,16 +174,13 @@ export class StateMachine {
/**
* Executes the state machine according to the specification
*/
async execute<T extends Document>(
executor: StateMachineExecutable,
context: MongoCryptContext
): Promise<T> {
async execute(executor: StateMachineExecutable, context: MongoCryptContext): Promise<Uint8Array> {
const keyVaultNamespace = executor._keyVaultNamespace;
const keyVaultClient = executor._keyVaultClient;
const metaDataClient = executor._metaDataClient;
const mongocryptdClient = executor._mongocryptdClient;
const mongocryptdManager = executor._mongocryptdManager;
let result: T | null = null;
let result: Uint8Array | null = null;

while (context.state !== MONGOCRYPT_CTX_DONE && context.state !== MONGOCRYPT_CTX_ERROR) {
debug(`[context#${context.id}] ${stateToString.get(context.state) || context.state}`);
Expand Down Expand Up @@ -211,16 +228,8 @@ export class StateMachine {
const keys = await this.fetchKeys(keyVaultClient, keyVaultNamespace, filter);

if (keys.length === 0) {
// This is kind of a hack. For `rewrapManyDataKey`, we have tests that
// guarantee that when there are no matching keys, `rewrapManyDataKey` returns
// nothing. We also have tests for auto encryption that guarantee for `encrypt`
// we return an error when there are no matching keys. This error is generated in
// subsequent iterations of the state machine.
// Some apis (`encrypt`) throw if there are no filter matches and others (`rewrapManyDataKey`)
// do not. We set the result manually here, and let the state machine continue. `libmongocrypt`
// will inform us if we need to error by setting the state to `MONGOCRYPT_CTX_ERROR` but
// otherwise we'll return `{ v: [] }`.
result = { v: [] } as any as T;
// See docs on EMPTY_V
result = EMPTY_V;
}
for await (const key of keys) {
context.addMongoOperationResponse(serialize(key));
Expand Down Expand Up @@ -252,7 +261,7 @@ export class StateMachine {
const message = context.status.message || 'Finalization error';
throw new MongoCryptError(message);
}
result = deserialize(finalizedContext, this.options) as T;
result = finalizedContext;
break;
}

Expand Down
Loading