Skip to content

Commit e2df185

Browse files
committed
feat(NODE-5191): add allowed hosts option
1 parent a6ad6cd commit e2df185

File tree

4 files changed

+118
-0
lines changed

4 files changed

+118
-0
lines changed

src/cmap/auth/mongo_credentials.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MongoInvalidArgumentError,
66
MongoMissingCredentialsError
77
} from '../../error';
8+
import { isString } from '../../utils';
89
import { GSSAPICanonicalizationValue } from './gssapi';
910
import type { OIDCRefreshFunction, OIDCRequestFunction } from './mongodb_oidc';
1011
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers';
@@ -30,6 +31,18 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism {
3031
return AuthMechanism.MONGODB_CR;
3132
}
3233

34+
const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.';
35+
36+
/** @internal */
37+
export const DEFAULT_ALLOWED_HOSTS = [
38+
'*.mongodb.net',
39+
'*.mongodb-dev.net',
40+
'*.mongodbgov.net',
41+
'localhost',
42+
'127.0.0.1',
43+
'::1'
44+
];
45+
3346
/** @public */
3447
export interface AuthMechanismProperties extends Document {
3548
SERVICE_HOST?: string;
@@ -103,6 +116,10 @@ export class MongoCredentials {
103116
}
104117
}
105118

119+
if (this.mechanism === AuthMechanism.MONGODB_OIDC && !this.mechanismProperties.ALLOWED_HOSTS) {
120+
this.mechanismProperties.ALLOWED_HOSTS = DEFAULT_ALLOWED_HOSTS;
121+
}
122+
106123
Object.freeze(this.mechanismProperties);
107124
Object.freeze(this);
108125
}
@@ -183,6 +200,18 @@ export class MongoCredentials {
183200
`Either a PROVIDER_NAME or a REQUEST_TOKEN_CALLBACK must be specified for mechanism '${this.mechanism}'.`
184201
);
185202
}
203+
204+
if (this.mechanismProperties.ALLOWED_HOSTS) {
205+
const hosts = this.mechanismProperties.ALLOWED_HOSTS;
206+
if (!Array.isArray(hosts)) {
207+
throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR);
208+
}
209+
for (const host of hosts) {
210+
if (!isString(host)) {
211+
throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR);
212+
}
213+
}
214+
}
186215
}
187216

188217
if (AUTH_MECHS_AUTH_SRC_EXTERNAL.has(this.mechanism)) {

src/connection_string.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,17 @@ export function parseOptions(
310310
);
311311
}
312312

313+
const uriMechanismProperties = urlOptions.get('authMechanismProperties');
314+
if (uriMechanismProperties) {
315+
for (const property of uriMechanismProperties) {
316+
if (property.includes('ALLOWED_HOSTS:')) {
317+
throw new MongoParseError(
318+
'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.'
319+
);
320+
}
321+
}
322+
}
323+
313324
if (objectOptions.has('loadBalanced')) {
314325
throw new MongoParseError('loadBalanced is only a valid option in the URI');
315326
}

src/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import type { Topology } from './sdam/topology';
3131
import type { ClientSession } from './sessions';
3232
import { WriteConcern } from './write_concern';
3333

34+
const OBJECT_STRING = '[object String]';
35+
3436
/**
3537
* MongoDB Driver style callback
3638
* @public
@@ -59,6 +61,15 @@ export const ByteUtils = {
5961
}
6062
};
6163

64+
/**
65+
* Use this to test if an object is a string. This is because
66+
* typeof new String('test') is 'object' and not 'string'.
67+
* @internal
68+
*/
69+
export function isString(value: any): boolean {
70+
return Object.prototype.toString.call(value) === OBJECT_STRING;
71+
}
72+
6273
/**
6374
* Throws if collectionName is not a valid mongodb collection namespace.
6475
* @internal

test/unit/connection_string.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as sinon from 'sinon';
55
import {
66
AUTH_MECHS_AUTH_SRC_EXTERNAL,
77
AuthMechanism,
8+
DEFAULT_ALLOWED_HOSTS,
89
FEATURE_FLAGS,
910
MongoAPIError,
1011
MongoClient,
@@ -210,6 +211,72 @@ describe('Connection String', function () {
210211
expect(options.readConcern.level).to.equal('local');
211212
});
212213

214+
context('when auth mechanism is MONGODB-OIDC', function () {
215+
context('when ALLOWED_HOSTS is in the URI', function () {
216+
it('raises an error', function () {
217+
expect(() => {
218+
parseOptions(
219+
'mongodb://localhost/?authMechanismProperties=PROVIDER_NAME:aws,ALLOWED_HOSTS:[localhost]&authMechanism=MONGODB-OIDC'
220+
);
221+
}).to.throw(
222+
MongoParseError,
223+
'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.'
224+
);
225+
});
226+
});
227+
228+
context('when ALLOWED_HOSTS is in the options', function () {
229+
context('when it is an array of strings', function () {
230+
const hosts = ['*.example.com'];
231+
232+
it('sets the allowed hosts property', function () {
233+
const options = parseOptions(
234+
'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws',
235+
{
236+
authMechanismProperties: {
237+
ALLOWED_HOSTS: hosts
238+
}
239+
}
240+
);
241+
expect(options.credentials.mechanismProperties).to.deep.equal({
242+
PROVIDER_NAME: 'aws',
243+
ALLOWED_HOSTS: hosts
244+
});
245+
});
246+
});
247+
248+
context('when it is not an array of strings', function () {
249+
it('raises an error', function () {
250+
expect(() => {
251+
parseOptions(
252+
'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws',
253+
{
254+
authMechanismProperties: {
255+
ALLOWED_HOSTS: [1, 2, 3]
256+
}
257+
}
258+
);
259+
}).to.throw(
260+
MongoInvalidArgumentError,
261+
'Auth mechanism property ALLOWED_HOSTS must be an array of strings.'
262+
);
263+
});
264+
});
265+
});
266+
267+
context('when ALLOWED_HOSTS is not in the options', function () {
268+
it('sets the default value', function () {
269+
const options = parseOptions(
270+
'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws'
271+
);
272+
expect(options.credentials.mechanismProperties).to.deep.equal({
273+
PROVIDER_NAME: 'aws',
274+
ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS
275+
});
276+
});
277+
});
278+
});
279+
213280
it('should parse `authMechanismProperties`', function () {
214281
const options = parseOptions(
215282
'mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,SERVICE_REALM:blah,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com&authMechanism=GSSAPI'

0 commit comments

Comments
 (0)