Skip to content

Commit e5abd54

Browse files
authored
chore: support Kerberos-specific CLI arguments MONGOSH-838 (#972)
1 parent 5dd2342 commit e5abd54

File tree

9 files changed

+210
-27
lines changed

9 files changed

+210
-27
lines changed

packages/cli-repl/src/arg-mapper.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,66 @@ describe('arg-mapper.mapCliToDriver', () => {
208208
});
209209
});
210210

211+
context('when the cli args have gssapiServiceName', () => {
212+
const cliOptions: CliOptions = { gssapiServiceName: 'alternate' };
213+
214+
it('maps to authMechanismProperties.SERVICE_NAME', async() => {
215+
expect(await mapCliToDriver(cliOptions)).to.deep.equal({
216+
authMechanismProperties: {
217+
SERVICE_NAME: 'alternate'
218+
}
219+
});
220+
});
221+
});
222+
223+
context('when the cli args have sspiRealmOverride', () => {
224+
const cliOptions: CliOptions = { sspiRealmOverride: 'REALM.COM' };
225+
226+
it('maps to authMechanismProperties.SERVICE_REALM', async() => {
227+
expect(await mapCliToDriver(cliOptions)).to.deep.equal({
228+
authMechanismProperties: {
229+
SERVICE_REALM: 'REALM.COM'
230+
}
231+
});
232+
});
233+
});
234+
235+
context('when the cli args have sspiHostnameCanonicalization', () => {
236+
context('with a value of none', () => {
237+
const cliOptions: CliOptions = { sspiHostnameCanonicalization: 'none' };
238+
239+
it('is not mapped to authMechanismProperties', async() => {
240+
expect(await mapCliToDriver(cliOptions)).to.deep.equal({});
241+
});
242+
});
243+
244+
context('with a value of forward', () => {
245+
const cliOptions: CliOptions = { sspiHostnameCanonicalization: 'forward' };
246+
247+
it('is mapped to authMechanismProperties', async() => {
248+
expect(await mapCliToDriver(cliOptions)).to.deep.equal({
249+
authMechanismProperties: {
250+
gssapiCanonicalizeHostName: 'true'
251+
}
252+
});
253+
});
254+
});
255+
256+
context('with a value of forwardAndReverse', () => {
257+
const cliOptions: CliOptions = { sspiHostnameCanonicalization: 'forwardAndReverse' };
258+
259+
it('is mapped to authMechanismProperties', async() => {
260+
try {
261+
await mapCliToDriver(cliOptions);
262+
} catch (e) {
263+
expect(e.message).to.contain('forwardAndReverse is not supported');
264+
return;
265+
}
266+
expect.fail('expected error');
267+
});
268+
});
269+
});
270+
211271
context('when the cli args have keyVaultNamespace', () => {
212272
const cliOptions: CliOptions = { keyVaultNamespace: 'db.datakeys' };
213273

packages/cli-repl/src/arg-mapper.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MongoshInvalidInputError, MongoshUnimplementedError } from '@mongosh/errors';
1+
import { CommonErrors, MongoshInvalidInputError, MongoshUnimplementedError } from '@mongosh/errors';
22
import { CliOptions, MongoClientOptions } from '@mongosh/service-provider-server';
33
import setValue from 'lodash.set';
44

@@ -13,6 +13,9 @@ const MAPPINGS = {
1313
awsSecretAccessKey: 'autoEncryption.kmsProviders.aws.secretAccessKey',
1414
awsSessionToken: 'autoEncryption.kmsProviders.aws.sessionToken',
1515
awsIamSessionToken: 'authMechanismProperties.AWS_SESSION_TOKEN',
16+
gssapiServiceName: 'authMechanismProperties.SERVICE_NAME',
17+
sspiRealmOverride: 'authMechanismProperties.SERVICE_REALM',
18+
sspiHostnameCanonicalization: { opt: 'authMechanismProperties.gssapiCanonicalizeHostName', fun: mapSspiHostnameCanonicalization },
1619
authenticationDatabase: 'authSource',
1720
authenticationMechanism: 'authMechanism',
1821
keyVaultNamespace: 'autoEncryption.keyVaultNamespace',
@@ -52,8 +55,16 @@ async function mapCliToDriver(options: CliOptions): Promise<MongoClientOptions>
5255
if (typeof mapping === 'object') {
5356
const cliValue = (options as any)[cliOption];
5457
if (cliValue) {
55-
const { opt, val } = mapping;
56-
setValue(nodeOptions, opt, val);
58+
let newValue: any;
59+
if ('val' in mapping) {
60+
newValue = mapping.val;
61+
} else {
62+
newValue = mapping.fun(cliValue);
63+
if (newValue === undefined) {
64+
return;
65+
}
66+
}
67+
setValue(nodeOptions, mapping.opt, newValue);
5768
}
5869
} else {
5970
setValue(nodeOptions, mapping, (options as any)[cliOption]);
@@ -112,4 +123,17 @@ function getCertificateExporter(): TlsCertificateExporter | undefined {
112123
return undefined;
113124
}
114125

126+
function mapSspiHostnameCanonicalization(value: string): string | undefined {
127+
if (!value || value === 'none') {
128+
return undefined;
129+
}
130+
if (value === 'forward') {
131+
return 'true';
132+
}
133+
throw new MongoshInvalidInputError(
134+
`--sspiHostnameCanonicalization value ${value} is not supported`,
135+
CommonErrors.InvalidArgument
136+
);
137+
}
138+
115139
export default mapCliToDriver;

packages/cli-repl/src/arg-parser.spec.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,12 +322,47 @@ describe('arg-parser', () => {
322322
context('when providing --gssapiHostName', () => {
323323
const argv = [ ...baseArgv, uri, '--gssapiHostName', 'example.com' ];
324324

325+
it('throws an error since it is not yet supported', () => {
326+
try {
327+
parseCliArgs(argv);
328+
} catch (e) {
329+
expect(e).to.be.instanceOf(MongoshUnimplementedError);
330+
expect(e.message).to.include('Argument --gssapiHostName is not yet supported in mongosh');
331+
return;
332+
}
333+
expect.fail('Expected error');
334+
});
335+
336+
// it('returns the URI in the object', () => {
337+
// expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri);
338+
// });
339+
340+
// it('sets the gssapiHostName in the object', () => {
341+
// expect(parseCliArgs(argv).gssapiHostName).to.equal('example.com');
342+
// });
343+
});
344+
345+
context('when providing --sspiHostnameCanonicalization', () => {
346+
const argv = [ ...baseArgv, uri, '--sspiHostnameCanonicalization', 'forward' ];
347+
348+
it('returns the URI in the object', () => {
349+
expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri);
350+
});
351+
352+
it('sets the gssapiHostName in the object', () => {
353+
expect(parseCliArgs(argv).sspiHostnameCanonicalization).to.equal('forward');
354+
});
355+
});
356+
357+
context('when providing --sspiRealmOverride', () => {
358+
const argv = [ ...baseArgv, uri, '--sspiRealmOverride', 'example2.com' ];
359+
325360
it('returns the URI in the object', () => {
326361
expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri);
327362
});
328363

329364
it('sets the gssapiHostName in the object', () => {
330-
expect(parseCliArgs(argv).gssapiHostName).to.equal('example.com');
365+
expect(parseCliArgs(argv).sspiRealmOverride).to.equal('example2.com');
331366
});
332367
});
333368

packages/cli-repl/src/arg-parser.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const OPTIONS = {
2727
'eval',
2828
'gssapiHostName',
2929
'gssapiServiceName',
30+
'sspiHostnameCanonicalization',
31+
'sspiRealmOverride',
3032
'host',
3133
'keyVaultNamespace',
3234
'kmsURL',
@@ -108,7 +110,8 @@ const DEPRECATED_ARGS_WITH_REPLACEMENT: Record<string, keyof CliOptions> = {
108110
*/
109111
const UNSUPPORTED_ARGS: Readonly<string[]> = [
110112
'sslFIPSMode',
111-
'tlsFIPSMode'
113+
'tlsFIPSMode',
114+
'gssapiHostName'
112115
];
113116

114117
/**

packages/cli-repl/src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export const USAGE = `
3838
--authenticationDatabase [arg] ${i18n.__('cli-repl.args.authenticationDatabase')}
3939
--authenticationMechanism [arg] ${i18n.__('cli-repl.args.authenticationMechanism')}
4040
--awsIamSessionToken [arg] ${i18n.__('cli-repl.args.awsIamSessionToken')}
41+
--gssapiServiceName [arg] ${i18n.__('cli-repl.args.gssapiServiceName')}
42+
--sspiHostnameCanonicalization [arg] ${i18n.__('cli-repl.args.sspiHostnameCanonicalization')}
43+
--sspiRealmOverride [arg] ${i18n.__('cli-repl.args.sspiRealmOverride')}
4144
4245
${clr(i18n.__('cli-repl.args.tlsOptions'), ['bold', 'yellow'])}
4346

packages/i18n/src/locales/en_US.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const translations: Catalog = {
3131
awsIamSessionToken: 'AWS IAM Temporary Session Token ID',
3232
gssapiServiceName: 'Service name to use when authenticating using GSSAPI/Kerberos',
3333
gssapiHostName: 'Remote host name to use for purpose of GSSAPI/Kerberos authentication',
34+
sspiHostnameCanonicalization: 'Specify the SSPI hostname canonicalization (none or forward, available on Windows)',
35+
sspiRealmOverride: 'Specify the SSPI server realm (available on Windows)',
3436
tlsOptions: 'TLS Options:',
3537
tls: 'Use TLS for all connections',
3638
tlsCertificateKeyFile: 'PEM certificate/key file for TLS',
@@ -82,7 +84,9 @@ const translations: Catalog = {
8284
},
8385
'uri-generator': {
8486
'no-host-port': 'If a full URI is provided, you cannot also specify --host or --port',
85-
'invalid-host': 'The --host argument contains an invalid character'
87+
'invalid-host': 'The --host argument contains an invalid character',
88+
'diverging-service-name': 'Either the --gssapiServiceName parameter or the SERVICE_NAME authentication mechanism property in the connection string can be used but not both.',
89+
'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>).',
8690
}
8791
},
8892
'service-provider-browser': {},

packages/service-provider-core/src/cli-options.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ export default interface CliOptions {
1818
awsSessionToken?: string;
1919
db?: string;
2020
eval?: string;
21-
gssapiHostName?: string;
2221
gssapiServiceName?: string;
22+
sspiHostnameCanonicalization?: string;
23+
sspiRealmOverride?: string;
2324
help?: boolean;
2425
host?: string;
2526
ipv6?: boolean;

packages/service-provider-core/src/uri-generator.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CommonErrors, MongoshInvalidInputError } from '@mongosh/errors';
22
import { expect } from 'chai';
3+
import CliOptions from './cli-options';
34
import generateUri from './uri-generator';
45

56
describe('uri-generator.generate-uri', () => {
@@ -77,6 +78,34 @@ describe('uri-generator.generate-uri', () => {
7778
}
7879
});
7980
});
81+
82+
context('when providing gssapiServiceName', () => {
83+
context('and the URI does not include SERVICE_NAME in authMechanismProperties', () => {
84+
const uri = 'mongodb+srv://some.host/foo';
85+
const options: CliOptions = { connectionSpecifier: uri, gssapiServiceName: 'alternate' };
86+
87+
it('does not throw an error', () => {
88+
expect(generateUri(options)).to.equal('mongodb+srv://some.host/foo');
89+
});
90+
});
91+
92+
context('and the URI includes SERVICE_NAME in authMechanismProperties', () => {
93+
const uri = 'mongodb+srv://some.host/foo?authMechanismProperties=SERVICE_NAME:whatever';
94+
const options: CliOptions = { connectionSpecifier: uri, gssapiServiceName: 'alternate' };
95+
96+
it('throws an error', () => {
97+
try {
98+
generateUri(options);
99+
} catch (e) {
100+
expect(e.name).to.equal('MongoshInvalidInputError');
101+
expect(e.code).to.equal(CommonErrors.InvalidArgument);
102+
expect(e.message).to.contain('--gssapiServiceName parameter or the SERVICE_NAME');
103+
return;
104+
}
105+
expect.fail('expected error');
106+
});
107+
});
108+
});
80109
});
81110

82111
context('when providing a URI with query parameters', () => {
@@ -120,6 +149,23 @@ describe('uri-generator.generate-uri', () => {
120149
expect(generateUri(options)).to.equal(uri);
121150
});
122151
});
152+
153+
context('when providing a URI with the legacy gssapiServiceName query parameter', () => {
154+
const uri = 'mongodb://192.42.42.42:27017,192.0.0.1:27018/db?gssapiServiceName=primary';
155+
const options = { connectionSpecifier: uri };
156+
157+
it('throws an error', () => {
158+
try {
159+
generateUri(options);
160+
} catch (e) {
161+
expect(e.name).to.equal('MongoshInvalidInputError');
162+
expect(e.code).to.equal(CommonErrors.InvalidArgument);
163+
expect(e.message).to.contain('gssapiServiceName query parameter is not supported');
164+
return;
165+
}
166+
expect.fail('expected error');
167+
});
168+
});
123169
});
124170

125171
context('when a URI is provided without a scheme', () => {

packages/service-provider-core/src/uri-generator.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,44 @@ const DEFAULT_HOST = '127.0.0.1';
2525
const DEFAULT_PORT = '27017';
2626

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

3332
/**
34-
* GSSAPI options not supported as options in Node driver,
35-
* only in the URI.
33+
* Invalid host message.
3634
*/
37-
// const GSSAPI_SERVICE_NAME = 'gssapiServiceName';
35+
const INVALID_HOST = 'cli-repl.uri-generator.invalid-host';
3836

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

4442
/**
45-
* Invalid host message.
43+
* Usage of unsupported gssapiServiceName query parameter
4644
*/
47-
const INVALID_HOST = 'cli-repl.uri-generator.invalid-host';
45+
const GSSAPI_SERVICE_NAME_UNSUPPORTED = 'cli-repl.uri-generator.gssapi-service-name-unsupported';
4846

4947
/**
5048
* Validate conflicts in the options.
51-
*
52-
* @param {CliOptions} options - The options.
5349
*/
54-
function validateConflicts(options: CliOptions): void {
50+
function validateConflicts(options: CliOptions, connectionString?: ConnectionString): void {
5551
if (options.host || options.port) {
5652
throw new MongoshInvalidInputError(i18n.__(CONFLICT), CommonErrors.InvalidArgument);
5753
}
54+
55+
if (options.gssapiServiceName && connectionString?.searchParams.has('authMechanismProperties')) {
56+
const authProperties = connectionString.searchParams.get('authMechanismProperties') ?? '';
57+
const serviceName = /,?SERVICE_NAME:([^,]+)/.exec(authProperties)?.[1];
58+
if (serviceName !== undefined && options.gssapiServiceName !== serviceName) {
59+
throw new MongoshInvalidInputError(i18n.__(DIVERGING_SERVICE_NAME), CommonErrors.InvalidArgument);
60+
}
61+
}
62+
63+
if (connectionString?.searchParams.has('gssapiServiceName')) {
64+
throw new MongoshInvalidInputError(i18n.__(GSSAPI_SERVICE_NAME_UNSUPPORTED), CommonErrors.InvalidArgument);
65+
}
5866
}
5967

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

161166
// mongodb+srv:// URI is provided, treat as correct and immediately return
162167
if (uri.startsWith(Scheme.MongoSrv)) {
163-
validateConflicts(options);
164-
return new ConnectionString(uri);
168+
const connectionString = new ConnectionString(uri);
169+
validateConflicts(options, connectionString);
170+
return connectionString;
165171
} else if (uri.startsWith(Scheme.Mongo)) {
166172
// we need to figure out if we have to add the directConnection query parameter
167-
validateConflicts(options);
168-
return addShellConnectionStringParameters(new ConnectionString(uri));
173+
const connectionString = new ConnectionString(uri);
174+
validateConflicts(options, connectionString);
175+
return addShellConnectionStringParameters(connectionString);
169176
}
170177

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

0 commit comments

Comments
 (0)