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
60 changes: 60 additions & 0 deletions packages/cli-repl/src/arg-mapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,66 @@ describe('arg-mapper.mapCliToDriver', () => {
});
});

context('when the cli args have gssapiServiceName', () => {
const cliOptions: CliOptions = { gssapiServiceName: 'alternate' };

it('maps to authMechanismProperties.SERVICE_NAME', async() => {
expect(await mapCliToDriver(cliOptions)).to.deep.equal({
authMechanismProperties: {
SERVICE_NAME: 'alternate'
}
});
});
});

context('when the cli args have sspiRealmOverride', () => {
const cliOptions: CliOptions = { sspiRealmOverride: 'REALM.COM' };

it('maps to authMechanismProperties.SERVICE_REALM', async() => {
expect(await mapCliToDriver(cliOptions)).to.deep.equal({
authMechanismProperties: {
SERVICE_REALM: 'REALM.COM'
}
});
});
});

context('when the cli args have sspiHostnameCanonicalization', () => {
context('with a value of none', () => {
const cliOptions: CliOptions = { sspiHostnameCanonicalization: 'none' };

it('is not mapped to authMechanismProperties', async() => {
expect(await mapCliToDriver(cliOptions)).to.deep.equal({});
});
});

context('with a value of forward', () => {
const cliOptions: CliOptions = { sspiHostnameCanonicalization: 'forward' };

it('is mapped to authMechanismProperties', async() => {
expect(await mapCliToDriver(cliOptions)).to.deep.equal({
authMechanismProperties: {
gssapiCanonicalizeHostName: 'true'
}
});
});
});

context('with a value of forwardAndReverse', () => {
const cliOptions: CliOptions = { sspiHostnameCanonicalization: 'forwardAndReverse' };

it('is mapped to authMechanismProperties', async() => {
try {
await mapCliToDriver(cliOptions);
} catch (e) {
expect(e.message).to.contain('forwardAndReverse is not supported');
return;
}
expect.fail('expected error');
});
});
});

context('when the cli args have keyVaultNamespace', () => {
const cliOptions: CliOptions = { keyVaultNamespace: 'db.datakeys' };

Expand Down
30 changes: 27 additions & 3 deletions packages/cli-repl/src/arg-mapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MongoshInvalidInputError, MongoshUnimplementedError } from '@mongosh/errors';
import { CommonErrors, MongoshInvalidInputError, MongoshUnimplementedError } from '@mongosh/errors';
import { CliOptions, MongoClientOptions } from '@mongosh/service-provider-server';
import setValue from 'lodash.set';

Expand All @@ -13,6 +13,9 @@ const MAPPINGS = {
awsSecretAccessKey: 'autoEncryption.kmsProviders.aws.secretAccessKey',
awsSessionToken: 'autoEncryption.kmsProviders.aws.sessionToken',
awsIamSessionToken: 'authMechanismProperties.AWS_SESSION_TOKEN',
gssapiServiceName: 'authMechanismProperties.SERVICE_NAME',
sspiRealmOverride: 'authMechanismProperties.SERVICE_REALM',
sspiHostnameCanonicalization: { opt: 'authMechanismProperties.gssapiCanonicalizeHostName', fun: mapSspiHostnameCanonicalization },
authenticationDatabase: 'authSource',
authenticationMechanism: 'authMechanism',
keyVaultNamespace: 'autoEncryption.keyVaultNamespace',
Expand Down Expand Up @@ -52,8 +55,16 @@ async function mapCliToDriver(options: CliOptions): Promise<MongoClientOptions>
if (typeof mapping === 'object') {
const cliValue = (options as any)[cliOption];
if (cliValue) {
const { opt, val } = mapping;
setValue(nodeOptions, opt, val);
let newValue: any;
if ('val' in mapping) {
newValue = mapping.val;
} else {
newValue = mapping.fun(cliValue);
if (newValue === undefined) {
return;
}
}
setValue(nodeOptions, mapping.opt, newValue);
}
} else {
setValue(nodeOptions, mapping, (options as any)[cliOption]);
Expand Down Expand Up @@ -112,4 +123,17 @@ function getCertificateExporter(): TlsCertificateExporter | undefined {
return undefined;
}

function mapSspiHostnameCanonicalization(value: string): string | undefined {
if (!value || value === 'none') {
return undefined;
}
if (value === 'forward') {
return 'true';
}
throw new MongoshInvalidInputError(
`--sspiHostnameCanonicalization value ${value} is not supported`,
CommonErrors.InvalidArgument
);
}

export default mapCliToDriver;
37 changes: 36 additions & 1 deletion packages/cli-repl/src/arg-parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,47 @@ describe('arg-parser', () => {
context('when providing --gssapiHostName', () => {
const argv = [ ...baseArgv, uri, '--gssapiHostName', 'example.com' ];

it('throws an error since it is not yet supported', () => {
try {
parseCliArgs(argv);
} catch (e) {
expect(e).to.be.instanceOf(MongoshUnimplementedError);
expect(e.message).to.include('Argument --gssapiHostName is not yet supported in mongosh');
return;
}
expect.fail('Expected error');
});

// it('returns the URI in the object', () => {
// expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri);
// });

// it('sets the gssapiHostName in the object', () => {
// expect(parseCliArgs(argv).gssapiHostName).to.equal('example.com');
// });
});

context('when providing --sspiHostnameCanonicalization', () => {
const argv = [ ...baseArgv, uri, '--sspiHostnameCanonicalization', 'forward' ];

it('returns the URI in the object', () => {
expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri);
});

it('sets the gssapiHostName in the object', () => {
expect(parseCliArgs(argv).sspiHostnameCanonicalization).to.equal('forward');
});
});

context('when providing --sspiRealmOverride', () => {
const argv = [ ...baseArgv, uri, '--sspiRealmOverride', 'example2.com' ];

it('returns the URI in the object', () => {
expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri);
});

it('sets the gssapiHostName in the object', () => {
expect(parseCliArgs(argv).gssapiHostName).to.equal('example.com');
expect(parseCliArgs(argv).sspiRealmOverride).to.equal('example2.com');
});
});

Expand Down
5 changes: 4 additions & 1 deletion packages/cli-repl/src/arg-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const OPTIONS = {
'eval',
'gssapiHostName',
'gssapiServiceName',
'sspiHostnameCanonicalization',
'sspiRealmOverride',
'host',
'keyVaultNamespace',
'kmsURL',
Expand Down Expand Up @@ -108,7 +110,8 @@ const DEPRECATED_ARGS_WITH_REPLACEMENT: Record<string, keyof CliOptions> = {
*/
const UNSUPPORTED_ARGS: Readonly<string[]> = [
'sslFIPSMode',
'tlsFIPSMode'
'tlsFIPSMode',
'gssapiHostName'
];

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-repl/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const USAGE = `
--authenticationDatabase [arg] ${i18n.__('cli-repl.args.authenticationDatabase')}
--authenticationMechanism [arg] ${i18n.__('cli-repl.args.authenticationMechanism')}
--awsIamSessionToken [arg] ${i18n.__('cli-repl.args.awsIamSessionToken')}
--gssapiServiceName [arg] ${i18n.__('cli-repl.args.gssapiServiceName')}
--sspiHostnameCanonicalization [arg] ${i18n.__('cli-repl.args.sspiHostnameCanonicalization')}
--sspiRealmOverride [arg] ${i18n.__('cli-repl.args.sspiRealmOverride')}

${clr(i18n.__('cli-repl.args.tlsOptions'), ['bold', 'yellow'])}

Expand Down
6 changes: 5 additions & 1 deletion packages/i18n/src/locales/en_US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const translations: Catalog = {
awsIamSessionToken: 'AWS IAM Temporary Session Token ID',
gssapiServiceName: 'Service name to use when authenticating using GSSAPI/Kerberos',
gssapiHostName: 'Remote host name to use for purpose of GSSAPI/Kerberos authentication',
sspiHostnameCanonicalization: 'Specify the SSPI hostname canonicalization (none or forward, available on Windows)',
sspiRealmOverride: 'Specify the SSPI server realm (available on Windows)',
tlsOptions: 'TLS Options:',
tls: 'Use TLS for all connections',
tlsCertificateKeyFile: 'PEM certificate/key file for TLS',
Expand Down Expand Up @@ -82,7 +84,9 @@ const translations: Catalog = {
},
'uri-generator': {
'no-host-port': 'If a full URI is provided, you cannot also specify --host or --port',
'invalid-host': 'The --host argument contains an invalid character'
'invalid-host': 'The --host argument contains an invalid character',
'diverging-service-name': 'Either the --gssapiServiceName parameter or the SERVICE_NAME authentication mechanism property in the connection string can be used but not both.',
'gssapi-service-name-unsupported': 'The gssapiServiceName query parameter is not supported anymore. Please use the --gssapiServiceName argument or the SERVICE_NAME authentication mechanism property (e.g. ?authMechanismProperties=SERVICE_NAME:<value>).',
}
},
'service-provider-browser': {},
Expand Down
3 changes: 2 additions & 1 deletion packages/service-provider-core/src/cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ export default interface CliOptions {
awsSessionToken?: string;
db?: string;
eval?: string;
gssapiHostName?: string;
gssapiServiceName?: string;
sspiHostnameCanonicalization?: string;
sspiRealmOverride?: string;
help?: boolean;
host?: string;
ipv6?: boolean;
Expand Down
46 changes: 46 additions & 0 deletions packages/service-provider-core/src/uri-generator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CommonErrors, MongoshInvalidInputError } from '@mongosh/errors';
import { expect } from 'chai';
import CliOptions from './cli-options';
import generateUri from './uri-generator';

describe('uri-generator.generate-uri', () => {
Expand Down Expand Up @@ -77,6 +78,34 @@ describe('uri-generator.generate-uri', () => {
}
});
});

context('when providing gssapiServiceName', () => {
context('and the URI does not include SERVICE_NAME in authMechanismProperties', () => {
const uri = 'mongodb+srv://some.host/foo';
const options: CliOptions = { connectionSpecifier: uri, gssapiServiceName: 'alternate' };

it('does not throw an error', () => {
expect(generateUri(options)).to.equal('mongodb+srv://some.host/foo');
});
});

context('and the URI includes SERVICE_NAME in authMechanismProperties', () => {
const uri = 'mongodb+srv://some.host/foo?authMechanismProperties=SERVICE_NAME:whatever';
const options: CliOptions = { connectionSpecifier: uri, gssapiServiceName: 'alternate' };

it('throws an error', () => {
try {
generateUri(options);
} catch (e) {
expect(e.name).to.equal('MongoshInvalidInputError');
expect(e.code).to.equal(CommonErrors.InvalidArgument);
expect(e.message).to.contain('--gssapiServiceName parameter or the SERVICE_NAME');
return;
}
expect.fail('expected error');
});
});
});
});

context('when providing a URI with query parameters', () => {
Expand Down Expand Up @@ -120,6 +149,23 @@ describe('uri-generator.generate-uri', () => {
expect(generateUri(options)).to.equal(uri);
});
});

context('when providing a URI with the legacy gssapiServiceName query parameter', () => {
const uri = 'mongodb://192.42.42.42:27017,192.0.0.1:27018/db?gssapiServiceName=primary';
const options = { connectionSpecifier: uri };

it('throws an error', () => {
try {
generateUri(options);
} catch (e) {
expect(e.name).to.equal('MongoshInvalidInputError');
expect(e.code).to.equal(CommonErrors.InvalidArgument);
expect(e.message).to.contain('gssapiServiceName query parameter is not supported');
return;
}
expect.fail('expected error');
});
});
});

context('when a URI is provided without a scheme', () => {
Expand Down
47 changes: 27 additions & 20 deletions packages/service-provider-core/src/uri-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,44 @@ const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PORT = '27017';

/**
* GSSAPI options not supported as options in Node driver,
* only in the URI.
* Conflicting host/port message.
*/
// const GSSAPI_HOST_NAME = 'gssapiHostName';
const CONFLICT = 'cli-repl.uri-generator.no-host-port';

/**
* GSSAPI options not supported as options in Node driver,
* only in the URI.
* Invalid host message.
*/
// const GSSAPI_SERVICE_NAME = 'gssapiServiceName';
const INVALID_HOST = 'cli-repl.uri-generator.invalid-host';

/**
* Conflicting host/port message.
* Diverging gssapiServiceName and SERVICE_NAME mechanism property
*/
const CONFLICT = 'cli-repl.uri-generator.no-host-port';
const DIVERGING_SERVICE_NAME = 'cli-repl.uri-generator.diverging-service-name';

/**
* Invalid host message.
* Usage of unsupported gssapiServiceName query parameter
*/
const INVALID_HOST = 'cli-repl.uri-generator.invalid-host';
const GSSAPI_SERVICE_NAME_UNSUPPORTED = 'cli-repl.uri-generator.gssapi-service-name-unsupported';

/**
* Validate conflicts in the options.
*
* @param {CliOptions} options - The options.
*/
function validateConflicts(options: CliOptions): void {
function validateConflicts(options: CliOptions, connectionString?: ConnectionString): void {
if (options.host || options.port) {
throw new MongoshInvalidInputError(i18n.__(CONFLICT), CommonErrors.InvalidArgument);
}

if (options.gssapiServiceName && connectionString?.searchParams.has('authMechanismProperties')) {
const authProperties = connectionString.searchParams.get('authMechanismProperties') ?? '';
const serviceName = /,?SERVICE_NAME:([^,]+)/.exec(authProperties)?.[1];
if (serviceName !== undefined && options.gssapiServiceName !== serviceName) {
throw new MongoshInvalidInputError(i18n.__(DIVERGING_SERVICE_NAME), CommonErrors.InvalidArgument);
}
}

if (connectionString?.searchParams.has('gssapiServiceName')) {
throw new MongoshInvalidInputError(i18n.__(GSSAPI_SERVICE_NAME_UNSUPPORTED), CommonErrors.InvalidArgument);
}
}

/**
Expand Down Expand Up @@ -120,9 +128,6 @@ function generatePort(options: CliOptions): string {
* only if one of these conditions is met:
* - it contains no '.' after the last appearance of '\' or '/'
* - it doesn't end in '.js' and it doesn't specify a path to an existing file
*
* gssapiHostName?: string; // needs to go in URI
* gssapiServiceName?: string; // needs to go in URI
*/
function generateUri(options: CliOptions): string {
if (options.nodb) {
Expand Down Expand Up @@ -160,12 +165,14 @@ function generateUriNormalized(options: CliOptions): ConnectionString {

// mongodb+srv:// URI is provided, treat as correct and immediately return
if (uri.startsWith(Scheme.MongoSrv)) {
validateConflicts(options);
return new ConnectionString(uri);
const connectionString = new ConnectionString(uri);
validateConflicts(options, connectionString);
return connectionString;
} else if (uri.startsWith(Scheme.Mongo)) {
// we need to figure out if we have to add the directConnection query parameter
validateConflicts(options);
return addShellConnectionStringParameters(new ConnectionString(uri));
const connectionString = new ConnectionString(uri);
validateConflicts(options, connectionString);
return addShellConnectionStringParameters(connectionString);
}

// Capture host, port and db from the string and generate a URI from
Expand Down