Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-2939): add new hostname canonicalization opts #3131

Merged
merged 26 commits into from Feb 17, 2022
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd09ac8
feat(NODE-2939): update hostname canonicalization opts
durran Feb 4, 2022
14298ac
test(NODE-2939): add additional kerberos tests
durran Feb 4, 2022
3c7ebe7
fix(NODE-2939): use host on cname error
durran Feb 4, 2022
9d4a659
test(NODE-2939): update spec tests
durran Feb 10, 2022
2816ce8
fix(NODE-2939): remove console log
durran Feb 10, 2022
a7c7ee5
fix(NODE-2939): dedup canonicalization props
durran Feb 10, 2022
407c0b2
test(NODE-2939): add skip reason for auth test
durran Feb 10, 2022
d02b688
fix(NODE-2939): remove console logs
durran Feb 10, 2022
6eb33ef
test(NODE-2939): fix spec tests
durran Feb 10, 2022
7e81758
test(NODE-2939): sync spec tests
durran Feb 10, 2022
e533001
test(NODE-2939): add spy checks to tests
durran Feb 11, 2022
e15acb5
fix(NODE-2939): change canonicalization to enum
durran Feb 11, 2022
66522ec
test(NODE-2939): update enum and tests
durran Feb 14, 2022
dbbd019
test(NODE-2939): update test expectations
durran Feb 14, 2022
5d84d87
feat(NODE-2939): make canonicalization values specific
durran Feb 14, 2022
ccc0865
test(NODE-2939): spy on fallback case
durran Feb 15, 2022
ce05266
test(NODE-2939): check empty and valid ptr returns
durran Feb 15, 2022
140c651
test(NODE-2939): more success cases
durran Feb 15, 2022
039d197
fix(NODE-2939): export enum and type
durran Feb 15, 2022
f94bfe8
test(NODE-2939): add cname lookup tests
durran Feb 15, 2022
a2bee66
test(NODE-2939): fix cname wrapping
durran Feb 15, 2022
fdeb7a7
refactor(NODE-2939): make the enum singular
durran Feb 15, 2022
abe949a
test(NODE-2939): empty ptr record falls back to host name
durran Feb 15, 2022
90fb44e
test(NODE-2939): recomment on macos
durran Feb 15, 2022
ff94b03
test(NODE-2939): assert on cname host param
durran Feb 15, 2022
8127b64
test(NODE-2939): add gssapi unit tests
durran Feb 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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(
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);
dariakp marked this conversation as resolved.
Show resolved Hide resolved
}
// 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.
dariakp marked this conversation as resolved.
Show resolved Hide resolved
resolveCname(host, callback);
}
}

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);
baileympearson marked this conversation as resolved.
Show resolved Hide resolved

// 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
184 changes: 173 additions & 11 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 @@ -79,18 +80,179 @@ describe('Kerberos', function () {
});
});

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();
context('when passing in CANONICALIZE_HOST_NAME', function () {
beforeEach(function () {
//if (process.platform === 'darwin') {
dariakp marked this conversation as resolved.
Show resolved Hide resolved
// this.currentTest.skipReason =
// 'DNS does not resolve with proper CNAME record on evergreen MacOS';
// this.skip();
//}
});

for (const option of [true, 'forward']) {
context(`when the value is ${option}`, function () {
it('authenticates with a forward 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);
expect(dns.resolveCname).to.be.calledOnce;
verifyKerberosAuthentication(client, done);
});
});
});
}
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);

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);
});
});
});
}

context('when the value is forwardAndReverse', 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:forwardAndReverse&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:forwardAndReverse&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:forwardAndReverse&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.calledOnce;
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:forwardAndReverse&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.calledOnce;
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:forwardAndReverse&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.calledOnce;
verifyKerberosAuthentication(client, done);
});
});
});
});
});

Expand Down