Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 2 additions & 6 deletions src/cmap/auth/mongo_credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import type { Document } from '../../bson';
import { MongoAPIError, MongoMissingCredentialsError } from '../../error';
import { AuthMechanism } from './providers';
import { $EXTERNAL_AUTH_SOURCE_MECHANISMS, AuthMechanism } from './providers';

// https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst
function getDefaultAuthMechanism(ismaster?: Document): AuthMechanism {
Expand Down Expand Up @@ -136,11 +136,7 @@ export class MongoCredentials {
throw new MongoMissingCredentialsError(`Username required for mechanism '${this.mechanism}'`);
}

if (
this.mechanism === AuthMechanism.MONGODB_GSSAPI ||
this.mechanism === AuthMechanism.MONGODB_AWS ||
this.mechanism === AuthMechanism.MONGODB_X509
) {
if ($EXTERNAL_AUTH_SOURCE_MECHANISMS.has(this.mechanism)) {
if (this.source != null && this.source !== '$external') {
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
throw new MongoAPIError(
Expand Down
7 changes: 7 additions & 0 deletions src/cmap/auth/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@ export const AuthMechanism = Object.freeze({
MONGODB_X509: 'MONGODB-X509'
} as const);

/** @public */
export const $EXTERNAL_AUTH_SOURCE_MECHANISMS = new Set<AuthMechanism | undefined>([
AuthMechanism.MONGODB_GSSAPI,
AuthMechanism.MONGODB_AWS,
AuthMechanism.MONGODB_X509
]);

/** @public */
export type AuthMechanism = typeof AuthMechanism[keyof typeof AuthMechanism];
12 changes: 7 additions & 5 deletions src/connection_string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { URLSearchParams } from 'url';

import type { Document } from './bson';
import { MongoCredentials } from './cmap/auth/mongo_credentials';
import { AuthMechanism } from './cmap/auth/providers';
import { $EXTERNAL_AUTH_SOURCE_MECHANISMS, AuthMechanism } from './cmap/auth/providers';
import { Compressor, CompressorName } from './cmap/wire_protocol/compression';
import { Encrypter } from './encrypter';
import { MongoAPIError, MongoInvalidArgumentError, MongoParseError } from './error';
Expand Down Expand Up @@ -125,7 +125,11 @@ export function resolveSRVRecord(options: MongoOptions, callback: Callback<HostA
const replicaSet = txtRecordOptions.get('replicaSet') ?? undefined;
const loadBalanced = txtRecordOptions.get('loadBalanced') ?? undefined;

if (!options.userSpecifiedAuthSource && source) {
if (
!options.userSpecifiedAuthSource &&
source &&
!$EXTERNAL_AUTH_SOURCE_MECHANISMS.has(options.credentials?.mechanism)
) {
options.credentials = MongoCredentials.merge(options.credentials, { source });
}

Expand Down Expand Up @@ -570,9 +574,7 @@ export const OPTIONS = {
let source = options.credentials?.source;
if (
mechanism === AuthMechanism.MONGODB_PLAIN ||
mechanism === AuthMechanism.MONGODB_GSSAPI ||
mechanism === AuthMechanism.MONGODB_AWS ||
mechanism === AuthMechanism.MONGODB_X509
$EXTERNAL_AUTH_SOURCE_MECHANISMS.has(mechanism)
) {
// some mechanisms have '$external' as the Auth Source
source = '$external';
Expand Down
7 changes: 7 additions & 0 deletions test/unit/connection_string.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ describe('Connection String', function () {
expect(options.credentials.mechanism).to.equal(AuthMechanism.MONGODB_GSSAPI);
});

it('should provide default authSource when valid AuthMechanism provided', function () {
const options = parseOptions(
'mongodb+srv://jira-sync.pw0q4.mongodb.net/testDB?authMechanism=MONGODB-AWS&retryWrites=true&w=majority'
);
expect(options.credentials.source).to.equal('$external');
});

it('should parse a numeric authSource with variable width', function () {
const options = parseOptions('mongodb://test@localhost/?authSource=0001');
expect(options.credentials.source).to.equal('0001');
Expand Down
119 changes: 119 additions & 0 deletions test/unit/srv_option_handling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { expect } from 'chai';
import * as dns from 'dns';
import * as sinon from 'sinon';
import { promisify } from 'util';

import { MongoCredentials } from '../../src/cmap/auth/mongo_credentials';
import { $EXTERNAL_AUTH_SOURCE_MECHANISMS, AuthMechanism } from '../../src/cmap/auth/providers';
import { resolveSRVRecord } from '../../src/connection_string';

const resolveSRVRecordAsync = promisify(resolveSRVRecord);

describe('Srv Option Handling', () => {
let resolveSrvStub: sinon.SinonStub;
let resolveTxtStub: sinon.SinonStub;
let resolveSRVRecordStub: sinon.SinonStub;
let lookupStub: sinon.SinonStub;

afterEach(async () => {
if (resolveSrvStub) {
resolveSrvStub.restore();
resolveSrvStub = undefined;
}
if (resolveTxtStub) {
resolveTxtStub.restore();
resolveTxtStub = undefined;
}
if (lookupStub) {
lookupStub.restore();
lookupStub = undefined;
}
if (resolveSRVRecordStub) {
resolveSRVRecordStub.restore();
resolveSRVRecordStub = undefined;
}
});

function makeStub(txtRecord: string) {
const mockAddress = [
{
name: 'localhost.test.mock.test.build.10gen.cc',
port: 2017,
weight: 0,
priority: 0
}
];

const mockRecord: string[][] = [[txtRecord]];

// first call is for the driver initial connection
// second call will check the poller
resolveSrvStub = sinon.stub(dns, 'resolveSrv').callsFake((address, callback) => {
return process.nextTick(callback, null, mockAddress);
});

resolveTxtStub = sinon.stub(dns, 'resolveTxt').callsFake((address, thisIsWhatWeAreTesting) => {
thisIsWhatWeAreTesting(null, mockRecord);
});
}

for (const mechanism of $EXTERNAL_AUTH_SOURCE_MECHANISMS) {
it('should set authSource to $external for ${mechanism} external mechanism:', async function () {
makeStub('authSource=thisShouldNotBeAuthSource');
const options = {
credentials: new MongoCredentials({
source: '$external',
mechanism: mechanism,
username: 'username',
password: 'password',
mechanismProperties: {}
}),
srvHost: 'test.mock.test.build.10gen.cc',
srvServiceName: 'mongodb',
userSpecifiedAuthSource: false
};

await resolveSRVRecordAsync(options as any);
expect(options).to.have.nested.property('credentials.source', '$external');
});
}

it('should set a default authSource for non-external mechanisms with no user-specified source:', async function () {
makeStub('authSource=thisShouldBeAuthSource');

const options = {
credentials: new MongoCredentials({
source: 'admin',
mechanism: AuthMechanism.MONGODB_SCRAM_SHA256,
username: 'username',
password: 'password',
mechanismProperties: {}
}),
srvHost: 'test.mock.test.build.10gen.cc',
srvServiceName: 'mongodb',
userSpecifiedAuthSource: false
};

await resolveSRVRecordAsync(options as any);
expect(options).to.have.nested.property('credentials.source', 'thisShouldBeAuthSource');
});

it('should retain credentials for any mechanism with no user-sepcificed source and no source in DNS:', async function () {
makeStub('');
const options = {
credentials: new MongoCredentials({
source: 'admin',
mechanism: AuthMechanism.MONGODB_SCRAM_SHA256,
username: 'username',
password: 'password',
mechanismProperties: {}
}),
srvHost: 'test.mock.test.build.10gen.cc',
srvServiceName: 'mongodb',
userSpecifiedAuthSource: false
};

await resolveSRVRecordAsync(options as any);
expect(options).to.have.nested.property('credentials.source', 'admin');
});
});