diff --git a/src/cmap/auth/gssapi.ts b/src/cmap/auth/gssapi.ts index 56d5206ccee..81d4514915e 100644 --- a/src/cmap/auth/gssapi.ts +++ b/src/cmap/auth/gssapi.ts @@ -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; @@ -93,7 +106,7 @@ function makeKerberosClient(authContext: AuthContext, callback: Callback { @@ -174,19 +187,52 @@ function finalize( }); } -function performGssapiCanonicalizeHostName( +export function performGSSAPICanonicalizeHostName( host: string, mechanismProperties: MechanismProperties, callback: Callback ): 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): 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]); } diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 326f98350e4..2c964c31195 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -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 @@ -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; } @@ -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( diff --git a/src/index.ts b/src/index.ts index 4a82c1bcbda..27c8ac944cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/test/manual/kerberos.test.js b/test/manual/kerberos.test.js index f60bde8140a..d52d6135f63 100644 --- a/test/manual/kerberos.test.js +++ b/test/manual/kerberos.test.js @@ -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'); @@ -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 diff --git a/test/spec/auth/connection-string.json b/test/spec/auth/connection-string.json index ea2bdf0ebae..300268a43d1 100644 --- a/test/spec/auth/connection-string.json +++ b/test/spec/auth/connection-string.json @@ -80,7 +80,7 @@ }, { "description": "should accept generic mechanism property (GSSAPI)", - "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com", "valid": true, "credential": { "username": "user@DOMAIN.COM", @@ -89,11 +89,46 @@ "mechanism": "GSSAPI", "mechanism_properties": { "SERVICE_NAME": "other", - "CANONICALIZE_HOST_NAME": true, - "SERVICE_HOST": "example.com" + "SERVICE_HOST": "example.com", + "CANONICALIZE_HOST_NAME": "forward" } } }, + { + "description": "should accept forwardAndReverse hostname canonicalization (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forwardAndReverse", + "valid": true, + "credential": { + "username": "user@DOMAIN.COM", + "password": null, + "source": "$external", + "mechanism": "GSSAPI", + "mechanism_properties": { + "SERVICE_NAME": "other", + "CANONICALIZE_HOST_NAME": "forwardAndReverse" + } + } + }, + { + "description": "should accept no hostname canonicalization (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:none", + "valid": true, + "credential": { + "username": "user@DOMAIN.COM", + "password": null, + "source": "$external", + "mechanism": "GSSAPI", + "mechanism_properties": { + "SERVICE_NAME": "other", + "CANONICALIZE_HOST_NAME": "none" + } + } + }, + { + "description": "must raise an error when the hostname canonicalization is invalid", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:invalid", + "valid": false + }, { "description": "should accept the password (GSSAPI)", "uri": "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external", @@ -108,6 +143,16 @@ } } }, + { + "description": "must raise an error when the authSource is empty", + "uri": "mongodb://user:password@localhost/foo?authSource=", + "valid": false + }, + { + "description": "must raise an error when the authSource is empty without credentials", + "uri": "mongodb://localhost/admin?authSource=", + "valid": false + }, { "description": "should throw an exception if authSource is invalid (GSSAPI)", "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo", @@ -207,6 +252,18 @@ "mechanism_properties": null } }, + { + "description": "should recognize the mechanism with no username when auth source is explicitly specified (MONGODB-X509)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-X509&authSource=$external", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-X509", + "mechanism_properties": null + } + }, { "description": "should throw an exception if supplied a password (MONGODB-X509)", "uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-X509", @@ -353,7 +410,7 @@ "credential": null }, { - "description": "authSource without username doesn't create credential", + "description": "authSource without username doesn't create credential (default mechanism)", "uri": "mongodb://localhost/?authSource=foo", "valid": true, "credential": null @@ -367,6 +424,62 @@ "description": "should throw an exception if no username/password provided (userinfo implies default mechanism)", "uri": "mongodb://:@localhost.com/", "valid": false + }, + { + "description": "should recognise the mechanism (MONGODB-AWS)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-AWS", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-AWS", + "mechanism_properties": null + } + }, + { + "description": "should recognise the mechanism when auth source is explicitly specified (MONGODB-AWS)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-AWS&authSource=$external", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-AWS", + "mechanism_properties": null + } + }, + { + "description": "should throw an exception if username and no password (MONGODB-AWS)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-AWS", + "valid": false, + "credential": null + }, + { + "description": "should use username and password if specified (MONGODB-AWS)", + "uri": "mongodb://user%21%40%23%24%25%5E%26%2A%28%29_%2B:pass%21%40%23%24%25%5E%26%2A%28%29_%2B@localhost/?authMechanism=MONGODB-AWS", + "valid": true, + "credential": { + "username": "user!@#$%^&*()_+", + "password": "pass!@#$%^&*()_+", + "source": "$external", + "mechanism": "MONGODB-AWS", + "mechanism_properties": null + } + }, + { + "description": "should use username, password and session token if specified (MONGODB-AWS)", + "uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token%21%40%23%24%25%5E%26%2A%28%29_%2B", + "valid": true, + "credential": { + "username": "user", + "password": "password", + "source": "$external", + "mechanism": "MONGODB-AWS", + "mechanism_properties": { + "AWS_SESSION_TOKEN": "token!@#$%^&*()_+" + } + } } ] } diff --git a/test/spec/auth/connection-string.yml b/test/spec/auth/connection-string.yml index 70a76afe9b1..408c76e373d 100644 --- a/test/spec/auth/connection-string.yml +++ b/test/spec/auth/connection-string.yml @@ -64,7 +64,7 @@ tests: SERVICE_NAME: "mongodb" - description: "should accept generic mechanism property (GSSAPI)" - uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com" + uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com" valid: true credential: username: "user@DOMAIN.COM" @@ -73,8 +73,36 @@ tests: mechanism: "GSSAPI" mechanism_properties: SERVICE_NAME: "other" - CANONICALIZE_HOST_NAME: true SERVICE_HOST: "example.com" + CANONICALIZE_HOST_NAME: "forward" + - + description: "should accept forwardAndReverse hostname canonicalization (GSSAPI)" + uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forwardAndReverse" + valid: true + credential: + username: "user@DOMAIN.COM" + password: ~ + source: "$external" + mechanism: "GSSAPI" + mechanism_properties: + SERVICE_NAME: "other" + CANONICALIZE_HOST_NAME: "forwardAndReverse" + - + description: "should accept no hostname canonicalization (GSSAPI)" + uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:none" + valid: true + credential: + username: "user@DOMAIN.COM" + password: ~ + source: "$external" + mechanism: "GSSAPI" + mechanism_properties: + SERVICE_NAME: "other" + CANONICALIZE_HOST_NAME: "none" + - + description: "must raise an error when the hostname canonicalization is invalid" + uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:invalid" + valid: false - description: "should accept the password (GSSAPI)" uri: "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external" @@ -86,6 +114,14 @@ tests: mechanism: "GSSAPI" mechanism_properties: SERVICE_NAME: "mongodb" + - + description: "must raise an error when the authSource is empty" + uri: "mongodb://user:password@localhost/foo?authSource=" + valid: false + - + description: "must raise an error when the authSource is empty without credentials" + uri: "mongodb://localhost/admin?authSource=" + valid: false - description: "should throw an exception if authSource is invalid (GSSAPI)" uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo" @@ -168,6 +204,16 @@ tests: source: "$external" mechanism: "MONGODB-X509" mechanism_properties: ~ + - + description: "should recognize the mechanism with no username when auth source is explicitly specified (MONGODB-X509)" + uri: "mongodb://localhost/?authMechanism=MONGODB-X509&authSource=$external" + valid: true + credential: + username: ~ + password: ~ + source: "$external" + mechanism: "MONGODB-X509" + mechanism_properties: ~ - description: "should throw an exception if supplied a password (MONGODB-X509)" uri: "mongodb://user:password@localhost/?authMechanism=MONGODB-X509" @@ -289,7 +335,7 @@ tests: valid: true credential: ~ - - description: "authSource without username doesn't create credential" + description: "authSource without username doesn't create credential (default mechanism)" uri: "mongodb://localhost/?authSource=foo" valid: true credential: ~ @@ -301,3 +347,49 @@ tests: description: "should throw an exception if no username/password provided (userinfo implies default mechanism)" uri: "mongodb://:@localhost.com/" valid: false + - + description: "should recognise the mechanism (MONGODB-AWS)" + uri: "mongodb://localhost/?authMechanism=MONGODB-AWS" + valid: true + credential: + username: ~ + password: ~ + source: "$external" + mechanism: "MONGODB-AWS" + mechanism_properties: ~ + - + description: "should recognise the mechanism when auth source is explicitly specified (MONGODB-AWS)" + uri: "mongodb://localhost/?authMechanism=MONGODB-AWS&authSource=$external" + valid: true + credential: + username: ~ + password: ~ + source: "$external" + mechanism: "MONGODB-AWS" + mechanism_properties: ~ + - + description: "should throw an exception if username and no password (MONGODB-AWS)" + uri: "mongodb://user@localhost/?authMechanism=MONGODB-AWS" + valid: false + credential: ~ + - + description: "should use username and password if specified (MONGODB-AWS)" + uri: "mongodb://user%21%40%23%24%25%5E%26%2A%28%29_%2B:pass%21%40%23%24%25%5E%26%2A%28%29_%2B@localhost/?authMechanism=MONGODB-AWS" + valid: true + credential: + username: "user!@#$%^&*()_+" + password: "pass!@#$%^&*()_+" + source: "$external" + mechanism: "MONGODB-AWS" + mechanism_properties: ~ + - + description: "should use username, password and session token if specified (MONGODB-AWS)" + uri: "mongodb://user:password@localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token%21%40%23%24%25%5E%26%2A%28%29_%2B" + valid: true + credential: + username: "user" + password: "password" + source: "$external" + mechanism: "MONGODB-AWS" + mechanism_properties: + AWS_SESSION_TOKEN: "token!@#$%^&*()_+" diff --git a/test/spec/connection-string/valid-auth.json b/test/spec/connection-string/valid-auth.json index 3b00b0e5031..176a54a096a 100644 --- a/test/spec/connection-string/valid-auth.json +++ b/test/spec/connection-string/valid-auth.json @@ -240,6 +240,27 @@ "authmechanism": "MONGODB-CR" } }, + { + "description": "Subdelimiters in user/pass don't need escaping (MONGODB-CR)", + "uri": "mongodb://!$&'()*+,;=:!$&'()*+,;=@127.0.0.1/admin?authMechanism=MONGODB-CR", + "valid": true, + "warning": false, + "hosts": [ + { + "type": "ipv4", + "host": "127.0.0.1", + "port": null + } + ], + "auth": { + "username": "!$&'()*+,;=", + "password": "!$&'()*+,;=", + "db": "admin" + }, + "options": { + "authmechanism": "MONGODB-CR" + } + }, { "description": "Escaped username (MONGODB-X509)", "uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509", @@ -263,7 +284,7 @@ }, { "description": "Escaped username (GSSAPI)", - "uri": "mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com&authMechanism=GSSAPI", + "uri": "mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com&authMechanism=GSSAPI", "valid": true, "warning": false, "hosts": [ @@ -282,8 +303,8 @@ "authmechanism": "GSSAPI", "authmechanismproperties": { "SERVICE_NAME": "other", - "CANONICALIZE_HOST_NAME": true, - "SERVICE_HOST": "example.com" + "SERVICE_HOST": "example.com", + "CANONICALIZE_HOST_NAME": "forward" } } }, diff --git a/test/spec/connection-string/valid-auth.yml b/test/spec/connection-string/valid-auth.yml index 2f8dc4da0f9..f40c748fa62 100644 --- a/test/spec/connection-string/valid-auth.yml +++ b/test/spec/connection-string/valid-auth.yml @@ -188,6 +188,22 @@ tests: db: "admin?" options: authmechanism: "MONGODB-CR" + - + description: "Subdelimiters in user/pass don't need escaping (MONGODB-CR)" + uri: "mongodb://!$&'()*+,;=:!$&'()*+,;=@127.0.0.1/admin?authMechanism=MONGODB-CR" + valid: true + warning: false + hosts: + - + type: "ipv4" + host: "127.0.0.1" + port: ~ + auth: + username: "!$&'()*+,;=" + password: "!$&'()*+,;=" + db: "admin" + options: + authmechanism: "MONGODB-CR" - description: "Escaped username (MONGODB-X509)" uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509" @@ -206,7 +222,7 @@ tests: authmechanism: "MONGODB-X509" - description: "Escaped username (GSSAPI)" - uri: "mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com&authMechanism=GSSAPI" + uri: "mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com&authMechanism=GSSAPI" valid: true warning: false hosts: @@ -222,8 +238,8 @@ tests: authmechanism: "GSSAPI" authmechanismproperties: SERVICE_NAME: "other" - CANONICALIZE_HOST_NAME: true, SERVICE_HOST: "example.com" + CANONICALIZE_HOST_NAME: "forward" - description: "At-signs in options aren't part of the userinfo" uri: "mongodb://alice:secret@example.com/admin?replicaset=my@replicaset" diff --git a/test/spec/uri-options/auth-options.json b/test/spec/uri-options/auth-options.json index 612d9144fb4..d7fa14a1346 100644 --- a/test/spec/uri-options/auth-options.json +++ b/test/spec/uri-options/auth-options.json @@ -2,7 +2,7 @@ "tests": [ { "description": "Valid auth options are parsed correctly (GSSAPI)", - "uri": "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com&authSource=$external", + "uri": "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com&authSource=$external", "valid": true, "warning": false, "hosts": null, @@ -11,8 +11,8 @@ "authMechanism": "GSSAPI", "authMechanismProperties": { "SERVICE_NAME": "other", - "CANONICALIZE_HOST_NAME": true, - "SERVICE_HOST": "example.com" + "SERVICE_HOST": "example.com", + "CANONICALIZE_HOST_NAME": "forward" }, "authSource": "$external" } diff --git a/test/spec/uri-options/auth-options.yml b/test/spec/uri-options/auth-options.yml index 45ac1e7915a..4a46516f136 100644 --- a/test/spec/uri-options/auth-options.yml +++ b/test/spec/uri-options/auth-options.yml @@ -1,7 +1,7 @@ tests: - description: "Valid auth options are parsed correctly (GSSAPI)" - uri: "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com&authSource=$external" + uri: "mongodb://foo:bar@example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com&authSource=$external" valid: true warning: false hosts: ~ @@ -10,8 +10,8 @@ tests: authMechanism: "GSSAPI" authMechanismProperties: SERVICE_NAME: "other" - CANONICALIZE_HOST_NAME: true, SERVICE_HOST: "example.com" + CANONICALIZE_HOST_NAME: "forward" authSource: "$external" - description: "Valid auth options are parsed correctly (SCRAM-SHA-1)" diff --git a/test/unit/assorted/auth.spec.test.ts b/test/unit/assorted/auth.spec.test.ts index 560ec28462b..e205bad3fcb 100644 --- a/test/unit/assorted/auth.spec.test.ts +++ b/test/unit/assorted/auth.spec.test.ts @@ -1,6 +1,8 @@ import { loadSpecTests } from '../../spec'; import { executeUriValidationTest } from '../../tools/uri_spec_runner'; +const SKIP = ['should throw an exception if username and no password (MONGODB-AWS)']; + describe('Auth option spec tests', function () { const suites = loadSpecTests('auth'); @@ -8,6 +10,10 @@ describe('Auth option spec tests', function () { describe(suite.name, function () { for (const test of suite.tests) { it(`${test.description}`, function () { + if (SKIP.includes(test.description)) { + this.test.skipReason = 'NODE-3986: Fix MONGODB-AWS Spec Test'; + this.skip(); + } executeUriValidationTest(test); }); } diff --git a/test/unit/cmap/auth/gssapi.test.js b/test/unit/cmap/auth/gssapi.test.js new file mode 100644 index 00000000000..4c84be0e30f --- /dev/null +++ b/test/unit/cmap/auth/gssapi.test.js @@ -0,0 +1,322 @@ +const chai = require('chai'); +const dns = require('dns'); +const sinon = require('sinon'); +const sinonChai = require('sinon-chai'); + +const { + GSSAPICanonicalizationValue, + performGSSAPICanonicalizeHostName, + resolveCname +} = require('../../../../src/cmap/auth/gssapi'); + +const expect = chai.expect; +chai.use(sinonChai); + +describe('GSSAPI', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + sandbox.spy(dns); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('.performGSSAPICanonicalizeHostName', () => { + const hostName = 'example.com'; + + for (const mode of [GSSAPICanonicalizationValue.off, GSSAPICanonicalizationValue.none]) { + context(`when the mode is ${mode}`, () => { + it('performs no dns lookups', done => { + performGSSAPICanonicalizeHostName( + hostName, + { CANONICALIZE_HOST_NAME: mode }, + (error, host) => { + if (error) return done(error); + expect(host).to.equal(hostName); + expect(dns.lookup).to.not.be.called; + expect(dns.resolvePtr).to.not.be.called; + expect(dns.resolveCname).to.not.be.called; + done(); + } + ); + }); + }); + } + + context(`when the mode is forward`, () => { + const resolved = '10gen.cc'; + const resolveStub = (host, callback) => { + callback(undefined, [resolved]); + }; + + beforeEach(() => { + dns.resolveCname.restore(); + sinon.stub(dns, 'resolveCname').callsFake(resolveStub); + }); + + it('performs a cname lookup', done => { + performGSSAPICanonicalizeHostName( + hostName, + { CANONICALIZE_HOST_NAME: GSSAPICanonicalizationValue.forward }, + (error, host) => { + if (error) return done(error); + expect(host).to.equal(resolved); + expect(dns.lookup).to.not.be.called; + expect(dns.resolvePtr).to.not.be.called; + expect(dns.resolveCname).to.be.calledOnceWith(hostName); + done(); + } + ); + }); + }); + + for (const mode of [ + GSSAPICanonicalizationValue.on, + GSSAPICanonicalizationValue.forwardAndReverse + ]) { + context(`when the mode is ${mode}`, () => { + context('when the forward lookup succeeds', () => { + const lookedUp = '1.1.1.1'; + const lookupStub = (host, callback) => { + callback(undefined, lookedUp); + }; + + context('when the reverse lookup succeeds', () => { + context('when there is 1 result', () => { + const resolved = '10gen.cc'; + const resolveStub = (host, callback) => { + callback(undefined, [resolved]); + }; + + beforeEach(() => { + dns.lookup.restore(); + dns.resolvePtr.restore(); + sinon.stub(dns, 'lookup').callsFake(lookupStub); + sinon.stub(dns, 'resolvePtr').callsFake(resolveStub); + }); + + it('uses the reverse lookup host', done => { + performGSSAPICanonicalizeHostName( + hostName, + { CANONICALIZE_HOST_NAME: mode }, + (error, host) => { + if (error) return done(error); + expect(host).to.equal(resolved); + expect(dns.lookup).to.be.calledOnceWith(hostName); + expect(dns.resolvePtr).to.be.calledOnceWith(lookedUp); + expect(dns.resolveCname).to.not.be.called; + done(); + } + ); + }); + }); + + context('when there is more than 1 result', () => { + const resolved = '10gen.cc'; + const resolveStub = (host, callback) => { + callback(undefined, [resolved, 'example.com']); + }; + + beforeEach(() => { + dns.lookup.restore(); + dns.resolvePtr.restore(); + sinon.stub(dns, 'lookup').callsFake(lookupStub); + sinon.stub(dns, 'resolvePtr').callsFake(resolveStub); + }); + + it('uses the first found reverse lookup host', done => { + performGSSAPICanonicalizeHostName( + hostName, + { CANONICALIZE_HOST_NAME: mode }, + (error, host) => { + if (error) return done(error); + expect(host).to.equal(resolved); + expect(dns.lookup).to.be.calledOnceWith(hostName); + expect(dns.resolvePtr).to.be.calledOnceWith(lookedUp); + expect(dns.resolveCname).to.not.be.called; + done(); + } + ); + }); + }); + }); + + context('when the reverse lookup fails', () => { + const cname = 'test.com'; + const resolveStub = (host, callback) => { + callback(new Error('failed'), undefined); + }; + const cnameStub = (host, callback) => { + callback(undefined, [cname]); + }; + + beforeEach(() => { + dns.lookup.restore(); + dns.resolvePtr.restore(); + dns.resolveCname.restore(); + sinon.stub(dns, 'lookup').callsFake(lookupStub); + sinon.stub(dns, 'resolvePtr').callsFake(resolveStub); + sinon.stub(dns, 'resolveCname').callsFake(cnameStub); + }); + + it('falls back to a cname lookup', done => { + performGSSAPICanonicalizeHostName( + hostName, + { CANONICALIZE_HOST_NAME: mode }, + (error, host) => { + if (error) return done(error); + expect(host).to.equal(cname); + expect(dns.lookup).to.be.calledOnceWith(hostName); + expect(dns.resolvePtr).to.be.calledOnceWith(lookedUp); + expect(dns.resolveCname).to.be.calledWith(hostName); + done(); + } + ); + }); + }); + + context('when the reverse lookup is empty', () => { + const resolveStub = (host, callback) => { + callback(undefined, []); + }; + + beforeEach(() => { + dns.lookup.restore(); + dns.resolvePtr.restore(); + sinon.stub(dns, 'lookup').callsFake(lookupStub); + sinon.stub(dns, 'resolvePtr').callsFake(resolveStub); + }); + + it('uses the provided host', done => { + performGSSAPICanonicalizeHostName( + hostName, + { CANONICALIZE_HOST_NAME: mode }, + (error, host) => { + if (error) return done(error); + expect(host).to.equal(hostName); + expect(dns.lookup).to.be.calledOnceWith(hostName); + expect(dns.resolvePtr).to.be.calledOnceWith(lookedUp); + expect(dns.resolveCname).to.not.be.called; + done(); + } + ); + }); + }); + }); + + context('when the forward lookup fails', () => { + const lookupStub = (host, callback) => { + callback(new Error('failed'), undefined); + }; + + beforeEach(() => { + dns.lookup.restore(); + sinon.stub(dns, 'lookup').callsFake(lookupStub); + }); + + it('fails with the error', done => { + performGSSAPICanonicalizeHostName(hostName, { CANONICALIZE_HOST_NAME: mode }, error => { + expect(error.message).to.equal('failed'); + expect(dns.lookup).to.be.calledOnceWith(hostName); + expect(dns.resolvePtr).to.not.be.called; + expect(dns.resolveCname).to.not.be.called; + done(); + }); + }); + }); + }); + } + }); + + describe('.resolveCname', () => { + context('when the cname call errors', () => { + const hostName = 'example.com'; + const resolveStub = (host, callback) => { + callback(new Error('failed')); + }; + + beforeEach(() => { + dns.resolveCname.restore(); + sinon.stub(dns, 'resolveCname').callsFake(resolveStub); + }); + + it('falls back to the provided host name', done => { + resolveCname(hostName, (error, host) => { + if (error) return done(error); + expect(host).to.equal(hostName); + expect(dns.resolveCname).to.be.calledOnceWith(hostName); + done(); + }); + }); + }); + + context('when the cname call returns results', () => { + context('when there is one result', () => { + const hostName = 'example.com'; + const resolved = '10gen.cc'; + const resolveStub = (host, callback) => { + callback(undefined, [resolved]); + }; + + beforeEach(() => { + dns.resolveCname.restore(); + sinon.stub(dns, 'resolveCname').callsFake(resolveStub); + }); + + it('uses the result', done => { + resolveCname(hostName, (error, host) => { + if (error) return done(error); + expect(host).to.equal(resolved); + expect(dns.resolveCname).to.be.calledOnceWith(hostName); + done(); + }); + }); + }); + + context('when there is more than one result', () => { + const hostName = 'example.com'; + const resolved = '10gen.cc'; + const resolveStub = (host, callback) => { + callback(undefined, [resolved, hostName]); + }; + + beforeEach(() => { + dns.resolveCname.restore(); + sinon.stub(dns, 'resolveCname').callsFake(resolveStub); + }); + + it('uses the first result', done => { + resolveCname(hostName, (error, host) => { + if (error) return done(error); + expect(host).to.equal(resolved); + expect(dns.resolveCname).to.be.calledOnceWith(hostName); + done(); + }); + }); + }); + }); + + context('when the cname call returns no results', () => { + const hostName = 'example.com'; + const resolveStub = (host, callback) => { + callback(undefined, []); + }; + + beforeEach(() => { + dns.resolveCname.restore(); + sinon.stub(dns, 'resolveCname').callsFake(resolveStub); + }); + + it('falls back to using the provided host', done => { + resolveCname(hostName, (error, host) => { + if (error) return done(error); + expect(host).to.equal(hostName); + expect(dns.resolveCname).to.be.calledOnceWith(hostName); + done(); + }); + }); + }); + }); +});