Skip to content

Commit

Permalink
feat(NODE-2939): add new hostname canonicalization opts (#3131)
Browse files Browse the repository at this point in the history
  • Loading branch information
durran committed Feb 17, 2022
1 parent aa069f1 commit d0390d0
Show file tree
Hide file tree
Showing 12 changed files with 822 additions and 37 deletions.
58 changes: 52 additions & 6 deletions src/cmap/auth/gssapi.ts
Expand Up @@ -12,10 +12,23 @@ import {
import { Callback, ns } from '../../utils';
import { AuthContext, AuthProvider } from './auth_provider';

/** @public */
export const GSSAPICanonicalizationValue = Object.freeze({
on: true,
off: false,
none: 'none',
forward: 'forward',
forwardAndReverse: 'forwardAndReverse'
} as const);

/** @public */
export type GSSAPICanonicalizationValue =
typeof GSSAPICanonicalizationValue[keyof typeof GSSAPICanonicalizationValue];

type MechanismProperties = {
/** @deprecated use `CANONICALIZE_HOST_NAME` instead */
gssapiCanonicalizeHostName?: boolean;
CANONICALIZE_HOST_NAME?: boolean;
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
SERVICE_HOST?: string;
SERVICE_NAME?: string;
SERVICE_REALM?: string;
Expand Down Expand Up @@ -93,7 +106,7 @@ function makeKerberosClient(authContext: AuthContext, callback: Callback<Kerbero

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

performGssapiCanonicalizeHostName(
performGSSAPICanonicalizeHostName(
hostAddress.host,
mechanismProperties,
(err?: Error | MongoError, host?: string) => {
Expand Down Expand Up @@ -174,19 +187,52 @@ function finalize(
});
}

function performGssapiCanonicalizeHostName(
export function performGSSAPICanonicalizeHostName(
host: string,
mechanismProperties: MechanismProperties,
callback: Callback<string>
): void {
if (!mechanismProperties.CANONICALIZE_HOST_NAME) return callback(undefined, host);
const mode = mechanismProperties.CANONICALIZE_HOST_NAME;
if (!mode || mode === GSSAPICanonicalizationValue.none) {
return callback(undefined, host);
}

// If forward and reverse or true
if (
mode === GSSAPICanonicalizationValue.on ||
mode === GSSAPICanonicalizationValue.forwardAndReverse
) {
// Perform the lookup of the ip address.
dns.lookup(host, (error, address) => {
// No ip found, return the error.
if (error) return callback(error);

// Perform a reverse ptr lookup on the ip address.
dns.resolvePtr(address, (err, results) => {
// This can error as ptr records may not exist for all ips. In this case
// fallback to a cname lookup as dns.lookup() does not return the
// cname.
if (err) {
return resolveCname(host, callback);
}
// If the ptr did not error but had no results, return the host.
callback(undefined, results.length > 0 ? results[0] : host);
});
});
} else {
// The case for forward is just to resolve the cname as dns.lookup()
// will not return it.
resolveCname(host, callback);
}
}

export function resolveCname(host: string, callback: Callback<string>): void {
// Attempt to resolve the host name
dns.resolveCname(host, (err, r) => {
if (err) return callback(err);
if (err) return callback(undefined, host);

// Get the first resolve host id
if (Array.isArray(r) && r.length > 0) {
if (r.length > 0) {
return callback(undefined, r[0]);
}

Expand Down
8 changes: 7 additions & 1 deletion src/cmap/auth/mongo_credentials.ts
Expand Up @@ -2,6 +2,7 @@
import type { Document } from '../../bson';
import { MongoAPIError, MongoMissingCredentialsError } from '../../error';
import { emitWarningOnce } from '../../utils';
import { GSSAPICanonicalizationValue } from './gssapi';
import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers';

// https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst
Expand Down Expand Up @@ -30,7 +31,7 @@ export interface AuthMechanismProperties extends Document {
SERVICE_HOST?: string;
SERVICE_NAME?: string;
SERVICE_REALM?: string;
CANONICALIZE_HOST_NAME?: boolean;
CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue;
AWS_SESSION_TOKEN?: string;
}

Expand Down Expand Up @@ -167,6 +168,11 @@ export class MongoCredentials {
// TODO(NODE-3485): Replace this with a MongoAuthValidationError
throw new MongoAPIError(`Password not allowed for mechanism MONGODB-X509`);
}

const canonicalization = this.mechanismProperties.CANONICALIZE_HOST_NAME ?? false;
if (!Object.values(GSSAPICanonicalizationValue).includes(canonicalization)) {
throw new MongoAPIError(`Invalid CANONICALIZE_HOST_NAME value: ${canonicalization}`);
}
}

static merge(
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -86,6 +86,7 @@ export {

// enums
export { BatchType } from './bulk/common';
export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi';
export { AuthMechanism } from './cmap/auth/providers';
export { Compressor } from './cmap/wire_protocol/compression';
export { CURSOR_FLAGS } from './cursor/abstract_cursor';
Expand Down
188 changes: 175 additions & 13 deletions test/manual/kerberos.test.js
Expand Up @@ -42,6 +42,7 @@ describe('Kerberos', function () {
}
let krb5Uri = process.env.MONGODB_URI;
const parts = krb5Uri.split('@', 2);
const host = parts[1].split('/')[0];

if (!process.env.KRB5_PRINCIPAL) {
console.error('skipping Kerberos tests, KRB5_PRINCIPAL environment variable is not defined');
Expand Down Expand Up @@ -74,24 +75,185 @@ describe('Kerberos', function () {
);
client.connect(function (err, client) {
if (err) return done(err);
expect(dns.resolveCname).to.be.calledOnce;
expect(dns.resolveCname).to.be.calledOnceWith(host);
verifyKerberosAuthentication(client, done);
});
});

it('validate that CANONICALIZE_HOST_NAME can be passed in', function (done) {
if (process.platform === 'darwin') {
this.test.skipReason = 'DNS does not resolve with proper CNAME record on evergreen MacOS';
this.skip();
}
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:true&maxPoolSize=1`
);
client.connect(function (err, client) {
if (err) return done(err);
expect(dns.resolveCname).to.be.calledOnce;
verifyKerberosAuthentication(client, done);
context('when passing in CANONICALIZE_HOST_NAME', function () {
beforeEach(function () {
if (process.platform === 'darwin') {
this.currentTest.skipReason =
'DNS does not resolve with proper CNAME record on evergreen MacOS';
this.skip();
}
});

context('when the value is forward', function () {
it('authenticates with a forward cname lookup', function (done) {
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:forward&maxPoolSize=1`
);
client.connect(function (err, client) {
if (err) return done(err);
expect(dns.resolveCname).to.be.calledOnceWith(host);
verifyKerberosAuthentication(client, done);
});
});
});

for (const option of [false, 'none']) {
context(`when the value is ${option}`, function () {
it('authenticates with no dns lookups', function (done) {
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
);
client.connect(function (err, client) {
if (err) return done(err);
expect(dns.resolveCname).to.not.be.called;
// 2 calls when establishing connection - expect no third call.
expect(dns.lookup).to.be.calledTwice;
verifyKerberosAuthentication(client, done);
});
});
});
}

for (const option of [true, 'forwardAndReverse']) {
context(`when the value is ${option}`, function () {
context('when the reverse lookup succeeds', function () {
const resolveStub = (address, callback) => {
callback(null, [host]);
};

beforeEach(function () {
dns.resolvePtr.restore();
sinon.stub(dns, 'resolvePtr').callsFake(resolveStub);
});

it('authenticates with a forward dns lookup and a reverse ptr lookup', function (done) {
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
);
client.connect(function (err, client) {
if (err) return done(err);
// 2 calls to establish connection, 1 call in canonicalization.
expect(dns.lookup).to.be.calledThrice;
expect(dns.resolvePtr).to.be.calledOnce;
verifyKerberosAuthentication(client, done);
});
});
});

context('when the reverse lookup is empty', function () {
const resolveStub = (address, callback) => {
callback(null, []);
};

beforeEach(function () {
dns.resolvePtr.restore();
sinon.stub(dns, 'resolvePtr').callsFake(resolveStub);
});

it('authenticates with a fallback cname lookup', function (done) {
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
);
client.connect(function (err, client) {
if (err) return done(err);
// 2 calls to establish connection, 1 call in canonicalization.
expect(dns.lookup).to.be.calledThrice;
// This fails.
expect(dns.resolvePtr).to.be.calledOnce;
// Expect the fallback to the host name.
expect(dns.resolveCname).to.not.be.called;
verifyKerberosAuthentication(client, done);
});
});
});

context('when the reverse lookup fails', function () {
const resolveStub = (address, callback) => {
callback(new Error('not found'), null);
};

beforeEach(function () {
dns.resolvePtr.restore();
sinon.stub(dns, 'resolvePtr').callsFake(resolveStub);
});

it('authenticates with a fallback cname lookup', function (done) {
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
);
client.connect(function (err, client) {
if (err) return done(err);
// 2 calls to establish connection, 1 call in canonicalization.
expect(dns.lookup).to.be.calledThrice;
// This fails.
expect(dns.resolvePtr).to.be.calledOnce;
// Expect the fallback to be called.
expect(dns.resolveCname).to.be.calledOnceWith(host);
verifyKerberosAuthentication(client, done);
});
});
});

context('when the cname lookup fails', function () {
const resolveStub = (address, callback) => {
callback(new Error('not found'), null);
};

beforeEach(function () {
dns.resolveCname.restore();
sinon.stub(dns, 'resolveCname').callsFake(resolveStub);
});

it('authenticates with a fallback host name', function (done) {
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
);
client.connect(function (err, client) {
if (err) return done(err);
// 2 calls to establish connection, 1 call in canonicalization.
expect(dns.lookup).to.be.calledThrice;
// This fails.
expect(dns.resolvePtr).to.be.calledOnce;
// Expect the fallback to be called.
expect(dns.resolveCname).to.be.calledOnceWith(host);
verifyKerberosAuthentication(client, done);
});
});
});

context('when the cname lookup is empty', function () {
const resolveStub = (address, callback) => {
callback(null, []);
};

beforeEach(function () {
dns.resolveCname.restore();
sinon.stub(dns, 'resolveCname').callsFake(resolveStub);
});

it('authenticates with a fallback host name', function (done) {
const client = new MongoClient(
`${krb5Uri}&authMechanismProperties=SERVICE_NAME:mongodb,CANONICALIZE_HOST_NAME:${option}&maxPoolSize=1`
);
client.connect(function (err, client) {
if (err) return done(err);
// 2 calls to establish connection, 1 call in canonicalization.
expect(dns.lookup).to.be.calledThrice;
// This fails.
expect(dns.resolvePtr).to.be.calledOnce;
// Expect the fallback to be called.
expect(dns.resolveCname).to.be.calledOnceWith(host);
verifyKerberosAuthentication(client, done);
});
});
});
});
}
});

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

0 comments on commit d0390d0

Please sign in to comment.