Skip to content

Commit d0390d0

Browse files
authored
feat(NODE-2939): add new hostname canonicalization opts (#3131)
1 parent aa069f1 commit d0390d0

File tree

12 files changed

+822
-37
lines changed

12 files changed

+822
-37
lines changed

src/cmap/auth/gssapi.ts

+52-6
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,23 @@ import {
1212
import { Callback, ns } from '../../utils';
1313
import { AuthContext, AuthProvider } from './auth_provider';
1414

15+
/** @public */
16+
export const GSSAPICanonicalizationValue = Object.freeze({
17+
on: true,
18+
off: false,
19+
none: 'none',
20+
forward: 'forward',
21+
forwardAndReverse: 'forwardAndReverse'
22+
} as const);
23+
24+
/** @public */
25+
export type GSSAPICanonicalizationValue =
26+
typeof GSSAPICanonicalizationValue[keyof typeof GSSAPICanonicalizationValue];
27+
1528
type MechanismProperties = {
1629
/** @deprecated use `CANONICALIZE_HOST_NAME` instead */
1730
gssapiCanonicalizeHostName?: boolean;
18-
CANONICALIZE_HOST_NAME?: boolean;
31+
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
1932
SERVICE_HOST?: string;
2033
SERVICE_NAME?: string;
2134
SERVICE_REALM?: string;
@@ -93,7 +106,7 @@ function makeKerberosClient(authContext: AuthContext, callback: Callback<Kerbero
93106

94107
const serviceName = mechanismProperties.SERVICE_NAME ?? 'mongodb';
95108

96-
performGssapiCanonicalizeHostName(
109+
performGSSAPICanonicalizeHostName(
97110
hostAddress.host,
98111
mechanismProperties,
99112
(err?: Error | MongoError, host?: string) => {
@@ -174,19 +187,52 @@ function finalize(
174187
});
175188
}
176189

177-
function performGssapiCanonicalizeHostName(
190+
export function performGSSAPICanonicalizeHostName(
178191
host: string,
179192
mechanismProperties: MechanismProperties,
180193
callback: Callback<string>
181194
): void {
182-
if (!mechanismProperties.CANONICALIZE_HOST_NAME) return callback(undefined, host);
195+
const mode = mechanismProperties.CANONICALIZE_HOST_NAME;
196+
if (!mode || mode === GSSAPICanonicalizationValue.none) {
197+
return callback(undefined, host);
198+
}
199+
200+
// If forward and reverse or true
201+
if (
202+
mode === GSSAPICanonicalizationValue.on ||
203+
mode === GSSAPICanonicalizationValue.forwardAndReverse
204+
) {
205+
// Perform the lookup of the ip address.
206+
dns.lookup(host, (error, address) => {
207+
// No ip found, return the error.
208+
if (error) return callback(error);
209+
210+
// Perform a reverse ptr lookup on the ip address.
211+
dns.resolvePtr(address, (err, results) => {
212+
// This can error as ptr records may not exist for all ips. In this case
213+
// fallback to a cname lookup as dns.lookup() does not return the
214+
// cname.
215+
if (err) {
216+
return resolveCname(host, callback);
217+
}
218+
// If the ptr did not error but had no results, return the host.
219+
callback(undefined, results.length > 0 ? results[0] : host);
220+
});
221+
});
222+
} else {
223+
// The case for forward is just to resolve the cname as dns.lookup()
224+
// will not return it.
225+
resolveCname(host, callback);
226+
}
227+
}
183228

229+
export function resolveCname(host: string, callback: Callback<string>): void {
184230
// Attempt to resolve the host name
185231
dns.resolveCname(host, (err, r) => {
186-
if (err) return callback(err);
232+
if (err) return callback(undefined, host);
187233

188234
// Get the first resolve host id
189-
if (Array.isArray(r) && r.length > 0) {
235+
if (r.length > 0) {
190236
return callback(undefined, r[0]);
191237
}
192238

src/cmap/auth/mongo_credentials.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { Document } from '../../bson';
33
import { MongoAPIError, MongoMissingCredentialsError } from '../../error';
44
import { emitWarningOnce } from '../../utils';
5+
import { GSSAPICanonicalizationValue } from './gssapi';
56
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers';
67

78
// https://github.com./mongodb/specifications/blob/master/source/auth/auth.rst
@@ -30,7 +31,7 @@ export interface AuthMechanismProperties extends Document {
3031
SERVICE_HOST?: string;
3132
SERVICE_NAME?: string;
3233
SERVICE_REALM?: string;
33-
CANONICALIZE_HOST_NAME?: boolean;
34+
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
3435
AWS_SESSION_TOKEN?: string;
3536
}
3637

@@ -167,6 +168,11 @@ export class MongoCredentials {
167168
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
168169
throw new MongoAPIError(`Password not allowed for mechanism MONGODB-X509`);
169170
}
171+
172+
const canonicalization = this.mechanismProperties.CANONICALIZE_HOST_NAME ?? false;
173+
if (!Object.values(GSSAPICanonicalizationValue).includes(canonicalization)) {
174+
throw new MongoAPIError(`Invalid CANONICALIZE_HOST_NAME value: ${canonicalization}`);
175+
}
170176
}
171177

172178
static merge(

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export {
8686

8787
// enums
8888
export { BatchType } from './bulk/common';
89+
export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi';
8990
export { AuthMechanism } from './cmap/auth/providers';
9091
export { Compressor } from './cmap/wire_protocol/compression';
9192
export { CURSOR_FLAGS } from './cursor/abstract_cursor';

test/manual/kerberos.test.js

+175-13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('Kerberos', function () {
4242
}
4343
let krb5Uri = process.env.MONGODB_URI;
4444
const parts = krb5Uri.split('@', 2);
45+
const host = parts[1].split('/')[0];
4546

4647
if (!process.env.KRB5_PRINCIPAL) {
4748
console.error('skipping Kerberos tests, KRB5_PRINCIPAL environment variable is not defined');
@@ -74,24 +75,185 @@ describe('Kerberos', function () {
7475
);
7576
client.connect(function (err, client) {
7677
if (err) return done(err);
77-
expect(dns.resolveCname).to.be.calledOnce;
78+
expect(dns.resolveCname).to.be.calledOnceWith(host);
7879
verifyKerberosAuthentication(client, done);
7980
});
8081
});
8182

82-
it('validate that CANONICALIZE_HOST_NAME can be passed in', function (done) {
83-
if (process.platform === 'darwin') {
84-
this.test.skipReason = 'DNS does not resolve with proper CNAME record on evergreen MacOS';
85-
this.skip();
86-
}
87-
const client = new MongoClient(
88-
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:true&maxPoolSize=1`
89-
);
90-
client.connect(function (err, client) {
91-
if (err) return done(err);
92-
expect(dns.resolveCname).to.be.calledOnce;
93-
verifyKerberosAuthentication(client, done);
83+
context('when passing in CANONICALIZE_HOST_NAME', function () {
84+
beforeEach(function () {
85+
if (process.platform === 'darwin') {
86+
this.currentTest.skipReason =
87+
'DNS does not resolve with proper CNAME record on evergreen MacOS';
88+
this.skip();
89+
}
90+
});
91+
92+
context('when the value is forward', function () {
93+
it('authenticates with a forward cname lookup', function (done) {
94+
const client = new MongoClient(
95+
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:forward&maxPoolSize=1`
96+
);
97+
client.connect(function (err, client) {
98+
if (err) return done(err);
99+
expect(dns.resolveCname).to.be.calledOnceWith(host);
100+
verifyKerberosAuthentication(client, done);
101+
});
102+
});
94103
});
104+
105+
for (const option of [false, 'none']) {
106+
context(`when the value is ${option}`, function () {
107+
it('authenticates with no dns lookups', function (done) {
108+
const client = new MongoClient(
109+
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
110+
);
111+
client.connect(function (err, client) {
112+
if (err) return done(err);
113+
expect(dns.resolveCname).to.not.be.called;
114+
// 2 calls when establishing connection - expect no third call.
115+
expect(dns.lookup).to.be.calledTwice;
116+
verifyKerberosAuthentication(client, done);
117+
});
118+
});
119+
});
120+
}
121+
122+
for (const option of [true, 'forwardAndReverse']) {
123+
context(`when the value is ${option}`, function () {
124+
context('when the reverse lookup succeeds', function () {
125+
const resolveStub = (address, callback) => {
126+
callback(null, [host]);
127+
};
128+
129+
beforeEach(function () {
130+
dns.resolvePtr.restore();
131+
sinon.stub(dns, 'resolvePtr').callsFake(resolveStub);
132+
});
133+
134+
it('authenticates with a forward dns lookup and a reverse ptr lookup', function (done) {
135+
const client = new MongoClient(
136+
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
137+
);
138+
client.connect(function (err, client) {
139+
if (err) return done(err);
140+
// 2 calls to establish connection, 1 call in canonicalization.
141+
expect(dns.lookup).to.be.calledThrice;
142+
expect(dns.resolvePtr).to.be.calledOnce;
143+
verifyKerberosAuthentication(client, done);
144+
});
145+
});
146+
});
147+
148+
context('when the reverse lookup is empty', function () {
149+
const resolveStub = (address, callback) => {
150+
callback(null, []);
151+
};
152+
153+
beforeEach(function () {
154+
dns.resolvePtr.restore();
155+
sinon.stub(dns, 'resolvePtr').callsFake(resolveStub);
156+
});
157+
158+
it('authenticates with a fallback cname lookup', function (done) {
159+
const client = new MongoClient(
160+
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
161+
);
162+
client.connect(function (err, client) {
163+
if (err) return done(err);
164+
// 2 calls to establish connection, 1 call in canonicalization.
165+
expect(dns.lookup).to.be.calledThrice;
166+
// This fails.
167+
expect(dns.resolvePtr).to.be.calledOnce;
168+
// Expect the fallback to the host name.
169+
expect(dns.resolveCname).to.not.be.called;
170+
verifyKerberosAuthentication(client, done);
171+
});
172+
});
173+
});
174+
175+
context('when the reverse lookup fails', function () {
176+
const resolveStub = (address, callback) => {
177+
callback(new Error('not found'), null);
178+
};
179+
180+
beforeEach(function () {
181+
dns.resolvePtr.restore();
182+
sinon.stub(dns, 'resolvePtr').callsFake(resolveStub);
183+
});
184+
185+
it('authenticates with a fallback cname lookup', function (done) {
186+
const client = new MongoClient(
187+
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
188+
);
189+
client.connect(function (err, client) {
190+
if (err) return done(err);
191+
// 2 calls to establish connection, 1 call in canonicalization.
192+
expect(dns.lookup).to.be.calledThrice;
193+
// This fails.
194+
expect(dns.resolvePtr).to.be.calledOnce;
195+
// Expect the fallback to be called.
196+
expect(dns.resolveCname).to.be.calledOnceWith(host);
197+
verifyKerberosAuthentication(client, done);
198+
});
199+
});
200+
});
201+
202+
context('when the cname lookup fails', function () {
203+
const resolveStub = (address, callback) => {
204+
callback(new Error('not found'), null);
205+
};
206+
207+
beforeEach(function () {
208+
dns.resolveCname.restore();
209+
sinon.stub(dns, 'resolveCname').callsFake(resolveStub);
210+
});
211+
212+
it('authenticates with a fallback host name', function (done) {
213+
const client = new MongoClient(
214+
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
215+
);
216+
client.connect(function (err, client) {
217+
if (err) return done(err);
218+
// 2 calls to establish connection, 1 call in canonicalization.
219+
expect(dns.lookup).to.be.calledThrice;
220+
// This fails.
221+
expect(dns.resolvePtr).to.be.calledOnce;
222+
// Expect the fallback to be called.
223+
expect(dns.resolveCname).to.be.calledOnceWith(host);
224+
verifyKerberosAuthentication(client, done);
225+
});
226+
});
227+
});
228+
229+
context('when the cname lookup is empty', function () {
230+
const resolveStub = (address, callback) => {
231+
callback(null, []);
232+
};
233+
234+
beforeEach(function () {
235+
dns.resolveCname.restore();
236+
sinon.stub(dns, 'resolveCname').callsFake(resolveStub);
237+
});
238+
239+
it('authenticates with a fallback host name', function (done) {
240+
const client = new MongoClient(
241+
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
242+
);
243+
client.connect(function (err, client) {
244+
if (err) return done(err);
245+
// 2 calls to establish connection, 1 call in canonicalization.
246+
expect(dns.lookup).to.be.calledThrice;
247+
// This fails.
248+
expect(dns.resolvePtr).to.be.calledOnce;
249+
// Expect the fallback to be called.
250+
expect(dns.resolveCname).to.be.calledOnceWith(host);
251+
verifyKerberosAuthentication(client, done);
252+
});
253+
});
254+
});
255+
});
256+
}
95257
});
96258

97259
// Unskip this test when a proper setup is available - see NODE-3060

0 commit comments

Comments
 (0)