Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions packages/service-provider-core/src/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ export default interface Admin {
*/
bsonLibrary: BSON;

/**
* Compute a hex MD5 hash from a string. Used for legacy auth mechanisms such as
* SCRAM-SHA-1.
*/
computeLegacyHexMD5?(str: string): Promise<string>;

/**
* list databases.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ const bsonlib = () => {
Decimal128,
BSONSymbol,
BSONRegExp,
UUID,
BSON,
} = driver;
return {
Expand All @@ -126,6 +127,7 @@ const bsonlib = () => {
BSONSymbol,
calculateObjectSize: BSON.calculateObjectSize,
EJSON: BSON.EJSON,
UUID,
BSONRegExp,
};
};
Expand Down
10 changes: 6 additions & 4 deletions packages/shell-api/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,10 +581,11 @@ export class Database<
if (writeConcern) {
command.writeConcern = writeConcern;
}
const digestPwd = processDigestPassword(
const digestPwd = await processDigestPassword(
user.user,
user.passwordDigestor,
command
command,
this._instanceState.currentServiceProvider
);
const orderedCmd = {
createUser: command.createUser,
Expand Down Expand Up @@ -627,10 +628,11 @@ export class Database<
if (writeConcern) {
command.writeConcern = writeConcern;
}
const digestPwd = processDigestPassword(
const digestPwd = await processDigestPassword(
username,
userDoc.passwordDigestor,
command
command,
this._instanceState.currentServiceProvider
);
const orderedCmd = {
updateUser: command.updateUser,
Expand Down
103 changes: 72 additions & 31 deletions packages/shell-api/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
DeleteOptions,
MapReduceOptions,
ExplainOptions,
ServiceProvider,
} from '@mongosh/service-provider-core';
import {
CommonErrors,
MongoshInvalidInputError,
MongoshUnimplementedError,
} from '@mongosh/errors';
import crypto from 'crypto';
import type { Database } from './database';
import type { Collection } from './collection';
import type { CursorIterationResult } from './result';
Expand All @@ -27,8 +27,12 @@
import type { AbstractFiniteCursor } from './abstract-cursor';
import type ChangeStreamCursor from './change-stream-cursor';
import type { BSON, ShellBson } from '@mongosh/shell-bson';
import { inspect } from 'util';
import type { MQLPipeline, MQLQuery } from './mql-types';
import type { InspectOptions } from 'util';

let coreUtilInspect: ((obj: any, options: InspectOptions) => string) & {
defaultOptions: InspectOptions;
};

/**
* Helper method to adapt aggregation pipeline options.
Expand Down Expand Up @@ -173,6 +177,35 @@
}, additions);
}

async function computeLegacyHexMD5(
sp: ServiceProvider,
str: string
): Promise<string> {
const digested = await sp.computeLegacyHexMD5?.(str);
if (digested) return digested;

// eslint-disable-next-line @typescript-eslint/consistent-type-imports
let crypto: typeof import('crypto');
try {
// Try to dynamically import crypto so that we don't break plain-JS-runtime builds.
// The Web Crypto API does not provide MD5, which is reasonable for a modern API
// but means that we cannot use it as a fallback.
crypto = require('crypto');
} catch {
throw new MongoshUnimplementedError(
'Legacy password digest algorithms like SCRAM-SHA-1 are not supported by this instance of mongosh',
CommonErrors.Deprecated
);
}
// NOTE: this code has raised a code scanning alert about the "use of a broken or weak cryptographic algorithm":
// we inherited this code from `mongo`, and we cannot replace MD5 with a different algorithm, since MD5 is part of the SCRAM-SHA-1 protocol,
// and the purpose of `passwordDigestor=client` is to improve the security of SCRAM-SHA-1, allowing the creation of new users
// without the need to communicate their password to the server.
const hash = crypto.createHash('md5');
hash.update(str);
return hash.digest('hex');
}

/**
* Optionally digest password if passwordDigestor field set to 'client'. If it's false,
* then hash the password.
Expand All @@ -181,11 +214,12 @@
* @param passwordDigestor
* @param {Object} command
*/
export function processDigestPassword(
export async function processDigestPassword(
username: string,
passwordDigestor: 'server' | 'client',
command: { pwd: string }
): { digestPassword?: boolean; pwd?: string } {
command: { pwd: string },
sp: ServiceProvider
): Promise<{ digestPassword?: boolean; pwd?: string }> {
if (passwordDigestor === undefined) {
return {};
}
Expand All @@ -202,14 +236,10 @@
CommonErrors.InvalidArgument
);
}
// NOTE: this code has raised a code scanning alert about the "use of a broken or weak cryptographic algorithm":
// we inherited this code from `mongo`, and we cannot replace MD5 with a different algorithm, since MD5 is part of the SCRAM-SHA-1 protocol,
// and the purpose of `passwordDigestor=client` is to improve the security of SCRAM-SHA-1, allowing the creation of new users
// without the need to communicate their password to the server.
const hash = crypto.createHash('md5');
hash.update(`${username}:mongo:${command.pwd}`);
const digested = hash.digest('hex');
return { digestPassword: false, pwd: digested };
return {
digestPassword: false,
pwd: await computeLegacyHexMD5(sp, `${username}:mongo:${command.pwd}`),
};
}
return { digestPassword: true };
}
Expand Down Expand Up @@ -630,24 +660,35 @@
'on shard': chunk.shard,
'last modified': chunk.lastmod,
} as any;
// Displaying a full, multi-line output for each chunk is a bit verbose,
// even if there are only a few chunks. Where supported, we use a custom
// inspection function to inspect a copy of this object with an unlimited
// line break length (i.e. all objects on a single line).
Object.defineProperty(
c,
Symbol.for('nodejs.util.inspect.custom'),
{
value: function (depth: number, options: any): string {
return inspect(
{ ...this },
{ ...options, breakLength: Infinity }
);
},
writable: true,
configurable: true,
}
);
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
coreUtilInspect ??= require('util').inspect;
} catch {
// No util.inspect available, e.g. in browser builds.
}
if (coreUtilInspect) {
// Displaying a full, multi-line output for each chunk is a bit verbose,
// even if there are only a few chunks. Where supported, we use a custom
// inspection function to inspect a copy of this object with an unlimited
// line break length (i.e. all objects on a single line).
Object.defineProperty(
c,
Symbol.for('nodejs.util.inspect.custom'),
{
value: function (
depth: number,
options: InspectOptions
): string {
return coreUtilInspect(
{ ...this },
{ ...options, breakLength: Infinity }
);
},
writable: true,
configurable: true,
}
);
}
if (chunk.jumbo) c.jumbo = 'yes';
chunksRes.push(c);
} else if (chunksRes.length === 20 && !verbose) {
Expand Down
2 changes: 1 addition & 1 deletion packages/shell-api/src/runtime-independence.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('Runtime independence', function () {
// for other environments, but which we should still ideally remove in the
// long run (and definitely not add anything here).
// Guaranteed bonusly for anyone who removes a package from this list!
const allowedNodeBuiltins = ['crypto', 'util', 'events', 'path'];
const allowedNodeBuiltins = ['events', 'path'];
// Our TextDecoder/TextEncoder polyfills require this, unfortunately.
context.Buffer = Buffer;

Expand Down
3 changes: 1 addition & 2 deletions packages/shell-api/src/shell-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import {
MongoshInternalError,
} from '@mongosh/errors';
import { DBQuery } from './dbquery';
import { promisify } from 'util';
import type { ClientSideFieldLevelEncryptionOptions } from './field-level-encryption';
import { dirname } from 'path';
import { ShellUserConfig } from '@mongosh/types';
Expand Down Expand Up @@ -422,7 +421,7 @@ export default class ShellApi extends ShellApiClass {

@returnsPromise
async sleep(ms: number): Promise<void> {
return await promisify(setTimeout)(ms);
return await new Promise<void>((resolve) => setTimeout(resolve, ms));
}

private async _print(
Expand Down
3 changes: 3 additions & 0 deletions packages/shell-bson/src/bson-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
calculateObjectSize,
Double,
EJSON,
UUID,
BSONRegExp,
} from 'bson';
export type {
Expand All @@ -29,6 +30,7 @@ export type {
Binary,
Double,
EJSON,
UUID,
BSONRegExp,
calculateObjectSize,
};
Expand All @@ -45,6 +47,7 @@ export type BSON = {
Binary: typeof Binary;
Double: typeof Double;
EJSON: typeof EJSON;
UUID: typeof UUID;
BSONRegExp: typeof BSONRegExp;
BSONSymbol: typeof BSONSymbol;
calculateObjectSize: typeof calculateObjectSize;
Expand Down
44 changes: 33 additions & 11 deletions packages/shell-bson/src/printable-bson.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
import type { BSON } from './';
import type { InspectOptionsStylized, CustomInspectFunction } from 'util';
import { inspect as utilInspect } from 'util';
import type {
InspectOptionsStylized,
CustomInspectFunction,
InspectOptions,
} from 'util';
const inspectCustom = Symbol.for('nodejs.util.inspect.custom');
type BSONClassKey = BSON[Exclude<
keyof BSON,
'EJSON' | 'calculateObjectSize'
>]['prototype']['_bsontype'];

let coreUtilInspect: ((obj: any, options: InspectOptions) => string) & {
defaultOptions: InspectOptions;
};
function inspectTypedArray(
obj: Iterable<number>,
options: InspectOptions
): string {
try {
coreUtilInspect ??= require('util').inspect;
return coreUtilInspect(obj, {
...options,
// These arrays can be very large, so would prefer to use the default options instead.
maxArrayLength: coreUtilInspect.defaultOptions.maxArrayLength,
});
} catch {
const arr = Array.from(obj);
if (arr.length > 100) {
return `[${arr.slice(0, 100).join(', ')}, ... ${
arr.length - 100
} more items]`;
}
return `[${arr.join(', ')}]`;
}
}

// Turn e.g. 'new Double(...)' into 'Double(...)' but preserve possible leading whitespace
function removeNewFromInspectResult(str: string): string {
return str.replace(/^(\s*)(new )/, '$1');
Expand Down Expand Up @@ -43,30 +71,24 @@ const makeBinaryVectorInspect = (bsonLibrary: BSON): CustomInspectFunction => {
switch (this.buffer[0]) {
case bsonLibrary.Binary.VECTOR_TYPE.Int8:
return `Binary.fromInt8Array(new Int8Array(${removeTypedArrayPrefixFromInspectResult(
utilInspect(this.toInt8Array(), {
inspectTypedArray(this.toInt8Array(), {
depth,
...options,
// These arrays can be very large, so would prefer to use the default options instead.
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
})
)}))`;
case bsonLibrary.Binary.VECTOR_TYPE.Float32:
return `Binary.fromFloat32Array(new Float32Array(${removeTypedArrayPrefixFromInspectResult(
utilInspect(this.toFloat32Array(), {
inspectTypedArray(this.toFloat32Array(), {
depth,
...options,
// These arrays can be very large, so would prefer to use the default options instead.
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
})
)}))`;
case bsonLibrary.Binary.VECTOR_TYPE.PackedBit: {
const paddingInfo = this.buffer[1] === 0 ? '' : `, ${this.buffer[1]}`;
return `Binary.fromPackedBits(new Uint8Array(${removeTypedArrayPrefixFromInspectResult(
utilInspect(this.toPackedBits(), {
inspectTypedArray(this.toPackedBits(), {
depth,
...options,
// These arrays can be very large, so would prefer to use the default options instead.
maxArrayLength: utilInspect.defaultOptions.maxArrayLength,
})
)})${paddingInfo})`;
}
Expand Down
10 changes: 4 additions & 6 deletions packages/shell-bson/src/shell-bson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
assignAll,
pickWithExactKeyMatch,
} from './helpers';
import { randomBytes } from 'crypto';

type LongWithoutAccidentallyExposedMethods = Omit<
typeof Long,
Expand Down Expand Up @@ -327,11 +326,10 @@ export function constructShellBson<
UUID: assignAll(
function UUID(hexstr?: string): BinaryType {
if (hexstr === undefined) {
// Generate a version 4, variant 1 UUID, like the old shell did.
const uuid = randomBytes(16);
uuid[6] = (uuid[6] & 0x0f) | 0x40;
uuid[8] = (uuid[8] & 0x3f) | 0x80;
hexstr = uuid.toString('hex');
// TODO(MONGOSH-2710): Actually use UUID instances from `bson`
// (but then also be consistent about that when we e.g. receive
// them from the server).
hexstr = new bson.UUID().toString();
}
assertArgsDefinedType([hexstr], ['string'], 'UUID');
// Strip any dashes, as they occur in the standard UUID formatting
Expand Down