diff --git a/.evergreen/setup-oidc-roles.sh b/.evergreen/setup-oidc-roles.sh index f3f2e0a0249..6be43905cf7 100644 --- a/.evergreen/setup-oidc-roles.sh +++ b/.evergreen/setup-oidc-roles.sh @@ -5,4 +5,4 @@ set -o xtrace # Write all commands first to stderr cd ${DRIVERS_TOOLS}/.evergreen/auth_oidc . ./activate-authoidcvenv.sh -${DRIVERS_TOOLS}/mongodb/bin/mongosh setup_oidc.js +${DRIVERS_TOOLS}/mongodb/bin/mongosh "mongodb://localhost:27017,localhost:27018/?replicaSet=oidc-repl0&readPreference=primary" setup_oidc.js diff --git a/src/cmap/auth/auth_provider.ts b/src/cmap/auth/auth_provider.ts index 2a38abe9b45..82942c0a9a4 100644 --- a/src/cmap/auth/auth_provider.ts +++ b/src/cmap/auth/auth_provider.ts @@ -5,14 +5,20 @@ import type { HandshakeDocument } from '../connect'; import type { Connection, ConnectionOptions } from '../connection'; import type { MongoCredentials } from './mongo_credentials'; +/** @internal */ export type AuthContextOptions = ConnectionOptions & ClientMetadataOptions; -/** Context used during authentication */ +/** + * Context used during authentication + * @internal + */ export class AuthContext { /** The connection to authenticate */ connection: Connection; /** The credentials to use for authentication */ credentials?: MongoCredentials; + /** If the context is for reauthentication. */ + reauthenticating = false; /** The options passed to the `connect` method */ options: AuthContextOptions; @@ -57,4 +63,22 @@ export class AuthProvider { // TODO(NODE-3483): Replace this with MongoMethodOverrideError callback(new MongoRuntimeError('`auth` method must be overridden by subclass')); } + + /** + * Reauthenticate. + * @param context - The shared auth context. + * @param callback - The callback. + */ + reauth(context: AuthContext, callback: Callback): void { + // If we are already reauthenticating this is a no-op. + if (context.reauthenticating) { + return callback(new MongoRuntimeError('Reauthentication already in progress.')); + } + context.reauthenticating = true; + const cb: Callback = (error, result) => { + context.reauthenticating = false; + callback(error, result); + }; + this.auth(context, cb); + } } diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index adfd5314114..b24c395fcc5 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -37,8 +37,11 @@ export interface AuthMechanismProperties extends Document { SERVICE_REALM?: string; CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue; AWS_SESSION_TOKEN?: string; + /** @experimental */ REQUEST_TOKEN_CALLBACK?: OIDCRequestFunction; + /** @experimental */ REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction; + /** @experimental */ PROVIDER_NAME?: 'aws'; } diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index be06df6d9e8..d5983ce6c6f 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -11,7 +11,10 @@ import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow'; import { CallbackWorkflow } from './mongodb_oidc/callback_workflow'; import type { Workflow } from './mongodb_oidc/workflow'; -/** @public */ +/** + * @public + * @experimental + */ export interface OIDCMechanismServerStep1 { authorizationEndpoint?: string; tokenEndpoint?: string; @@ -21,21 +24,30 @@ export interface OIDCMechanismServerStep1 { requestScopes?: string[]; } -/** @public */ +/** + * @public + * @experimental + */ export interface OIDCRequestTokenResult { accessToken: string; expiresInSeconds?: number; refreshToken?: string; } -/** @public */ +/** + * @public + * @experimental + */ export type OIDCRequestFunction = ( principalName: string, serverResult: OIDCMechanismServerStep1, timeout: AbortSignal | number ) => Promise; -/** @public */ +/** + * @public + * @experimental + */ export type OIDCRefreshFunction = ( principalName: string, serverResult: OIDCMechanismServerStep1, @@ -52,6 +64,7 @@ OIDC_WORKFLOWS.set('aws', new AwsServiceWorkflow()); /** * OIDC auth provider. + * @experimental */ export class MongoDBOIDC extends AuthProvider { /** @@ -65,7 +78,7 @@ export class MongoDBOIDC extends AuthProvider { * Authenticate using OIDC */ override auth(authContext: AuthContext, callback: Callback): void { - const { connection, credentials, response } = authContext; + const { connection, credentials, response, reauthenticating } = authContext; if (response?.speculativeAuthenticate) { return callback(); @@ -86,7 +99,7 @@ export class MongoDBOIDC extends AuthProvider { ) ); } - workflow.execute(connection, credentials).then( + workflow.execute(connection, credentials, reauthenticating).then( result => { return callback(undefined, result); }, diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 59da9eb2501..ffebe7e49bd 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -58,7 +58,11 @@ export class CallbackWorkflow implements Workflow { * - put the new entry in the cache. * - execute step two. */ - async execute(connection: Connection, credentials: MongoCredentials): Promise { + async execute( + connection: Connection, + credentials: MongoCredentials, + reauthenticate = false + ): Promise { const request = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; const refresh = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; @@ -69,8 +73,8 @@ export class CallbackWorkflow implements Workflow { refresh || null ); if (entry) { - // Check if the entry is not expired. - if (entry.isValid()) { + // Check if the entry is not expired and if we are reauthenticating. + if (!reauthenticate && entry.isValid()) { // Skip step one and execute the step two saslContinue. try { const result = await finishAuth(entry.tokenResult, undefined, connection, credentials); diff --git a/src/cmap/auth/mongodb_oidc/workflow.ts b/src/cmap/auth/mongodb_oidc/workflow.ts index 8b88ee00a84..68d0b97688a 100644 --- a/src/cmap/auth/mongodb_oidc/workflow.ts +++ b/src/cmap/auth/mongodb_oidc/workflow.ts @@ -8,7 +8,11 @@ export interface Workflow { * All device workflows must implement this method in order to get the access * token and then call authenticate with it. */ - execute(connection: Connection, credentials: MongoCredentials): Promise; + execute( + connection: Connection, + credentials: MongoCredentials, + reauthenticate?: boolean + ): Promise; /** * Get the document to add for speculative authentication. diff --git a/src/cmap/auth/providers.ts b/src/cmap/auth/providers.ts index 74e3638ecc5..d01c06324bb 100644 --- a/src/cmap/auth/providers.ts +++ b/src/cmap/auth/providers.ts @@ -8,6 +8,7 @@ export const AuthMechanism = Object.freeze({ MONGODB_SCRAM_SHA1: 'SCRAM-SHA-1', MONGODB_SCRAM_SHA256: 'SCRAM-SHA-256', MONGODB_X509: 'MONGODB-X509', + /** @experimental */ MONGODB_OIDC: 'MONGODB-OIDC' } as const); diff --git a/src/cmap/auth/scram.ts b/src/cmap/auth/scram.ts index 7a339151fa9..dbe4ce25a39 100644 --- a/src/cmap/auth/scram.ts +++ b/src/cmap/auth/scram.ts @@ -53,8 +53,8 @@ class ScramSHA extends AuthProvider { } override auth(authContext: AuthContext, callback: Callback) { - const response = authContext.response; - if (response && response.speculativeAuthenticate) { + const { reauthenticating, response } = authContext; + if (response?.speculativeAuthenticate && !reauthenticating) { continueScramConversation( this.cryptoMethod, response.speculativeAuthenticate, diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index 278864cda25..e7e7a87c896 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -36,7 +36,8 @@ import { MIN_SUPPORTED_WIRE_VERSION } from './wire_protocol/constants'; -const AUTH_PROVIDERS = new Map([ +/** @internal */ +export const AUTH_PROVIDERS = new Map([ [AuthMechanism.MONGODB_AWS, new MongoDBAWS()], [AuthMechanism.MONGODB_CR, new MongoCR()], [AuthMechanism.MONGODB_GSSAPI, new GSSAPI()], @@ -117,6 +118,7 @@ function performInitialHandshake( } const authContext = new AuthContext(conn, credentials, options); + conn.authContext = authContext; prepareHandshakeDocument(authContext, (err, handshakeDoc) => { if (err || !handshakeDoc) { return callback(err); diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 8557f9275dd..464f86d9b39 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -37,6 +37,7 @@ import { uuidV4 } from '../utils'; import type { WriteConcern } from '../write_concern'; +import type { AuthContext } from './auth/auth_provider'; import type { MongoCredentials } from './auth/mongo_credentials'; import { CommandFailedEvent, @@ -126,7 +127,6 @@ export interface ConnectionOptions noDelay?: boolean; socketTimeoutMS?: number; cancellationToken?: CancellationToken; - metadata: ClientMetadata; } @@ -164,6 +164,8 @@ export class Connection extends TypedEventEmitter { cmd: Document, options: CommandOptions | undefined ) => Promise; + /** @internal */ + authContext?: AuthContext; /**@internal */ [kDelayedTimeoutId]: NodeJS.Timeout | null; diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index 3991f378685..5365d19d07c 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -16,8 +16,11 @@ import { CONNECTION_READY } from '../constants'; import { + AnyError, + MONGODB_ERROR_CODES, MongoError, MongoInvalidArgumentError, + MongoMissingCredentialsError, MongoNetworkError, MongoRuntimeError, MongoServerError @@ -25,7 +28,7 @@ import { import { CancellationToken, TypedEventEmitter } from '../mongo_types'; import type { Server } from '../sdam/server'; import { Callback, eachAsync, List, makeCounter } from '../utils'; -import { connect } from './connect'; +import { AUTH_PROVIDERS, connect } from './connect'; import { Connection, ConnectionEvents, ConnectionOptions } from './connection'; import { ConnectionCheckedInEvent, @@ -537,32 +540,30 @@ export class ConnectionPool extends TypedEventEmitter { withConnection( conn: Connection | undefined, fn: WithConnectionCallback, - callback?: Callback + callback: Callback ): void { if (conn) { // use the provided connection, and do _not_ check it in after execution fn(undefined, conn, (fnErr, result) => { - if (typeof callback === 'function') { - if (fnErr) { - callback(fnErr); - } else { - callback(undefined, result); - } + if (fnErr) { + return this.withReauthentication(fnErr, conn, fn, callback); } + callback(undefined, result); }); - return; } this.checkOut((err, conn) => { // don't callback with `err` here, we might want to act upon it inside `fn` fn(err as MongoError, conn, (fnErr, result) => { - if (typeof callback === 'function') { - if (fnErr) { - callback(fnErr); + if (fnErr) { + if (conn) { + this.withReauthentication(fnErr, conn, fn, callback); } else { - callback(undefined, result); + callback(fnErr); } + } else { + callback(undefined, result); } if (conn) { @@ -572,6 +573,66 @@ export class ConnectionPool extends TypedEventEmitter { }); } + private withReauthentication( + fnErr: AnyError, + conn: Connection, + fn: WithConnectionCallback, + callback: Callback + ) { + if (fnErr instanceof MongoError && fnErr.code === MONGODB_ERROR_CODES.Reauthenticate) { + this.reauthenticate(conn, fn, (error, res) => { + if (error) { + return callback(error); + } + callback(undefined, res); + }); + } else { + callback(fnErr); + } + } + + /** + * Reauthenticate on the same connection and then retry the operation. + */ + private reauthenticate( + connection: Connection, + fn: WithConnectionCallback, + callback: Callback + ): void { + const authContext = connection.authContext; + if (!authContext) { + return callback(new MongoRuntimeError('No auth context found on connection.')); + } + const credentials = authContext.credentials; + if (!credentials) { + return callback( + new MongoMissingCredentialsError( + 'Connection is missing credentials when asked to reauthenticate' + ) + ); + } + const resolvedCredentials = credentials.resolveAuthMechanism(connection.hello || undefined); + const provider = AUTH_PROVIDERS.get(resolvedCredentials.mechanism); + if (!provider) { + return callback( + new MongoMissingCredentialsError( + `Reauthenticate failed due to no auth provider for ${credentials.mechanism}` + ) + ); + } + provider.reauth(authContext, error => { + if (error) { + return callback(error); + } + return fn(undefined, connection, (fnErr, fnResult) => { + if (fnErr) { + return callback(fnErr); + } + callback(undefined, fnResult); + }); + }); + } + /** Clear the min pool size timer */ private clearMinPoolSizeTimer(): void { const minPoolSizeTimer = this[kMinPoolSizeTimer]; diff --git a/src/error.ts b/src/error.ts index 0e9590423f7..f40dfd5229d 100644 --- a/src/error.ts +++ b/src/error.ts @@ -58,7 +58,8 @@ export const MONGODB_ERROR_CODES = Object.freeze({ IllegalOperation: 20, MaxTimeMSExpired: 50, UnknownReplWriteConcern: 79, - UnsatisfiableWriteConcern: 100 + UnsatisfiableWriteConcern: 100, + Reauthenticate: 391 } as const); // From spec@https://github.com/mongodb/specifications/blob/f93d78191f3db2898a59013a7ed5650352ef6da8/source/change-streams/change-streams.rst#resumable-error diff --git a/src/index.ts b/src/index.ts index 22ab7f4b994..2ea5b261240 100644 --- a/src/index.ts +++ b/src/index.ts @@ -197,6 +197,7 @@ export type { ResumeToken, UpdateDescription } from './change_stream'; +export type { AuthContext, AuthContextOptions } from './cmap/auth/auth_provider'; export type { AuthMechanismProperties, MongoCredentials, diff --git a/test/integration/auth/auth.spec.test.ts b/test/integration/auth/auth.spec.test.ts new file mode 100644 index 00000000000..cfce338e8d7 --- /dev/null +++ b/test/integration/auth/auth.spec.test.ts @@ -0,0 +1,8 @@ +import * as path from 'path'; + +import { loadSpecTests } from '../../spec'; +import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; + +describe('Auth (unified)', function () { + runUnifiedSuite(loadSpecTests(path.join('auth', 'unified'))); +}); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index f89eec10019..ce5728e597b 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -452,5 +452,164 @@ describe('MONGODB-OIDC', function () { }); }); }); + + // The driver MUST test reauthentication with MONGODB-OIDC for a read operation. + describe('6. Reauthentication', function () { + let refreshInvocations = 0; + let findStarted = 0; + let findSucceeded = 0; + let findFailed = 0; + let saslStarted = 0; + let saslSucceeded = 0; + let client; + let collection; + const cache = OIDC_WORKFLOWS.get('callback').cache; + + // - Create request and refresh callbacks that return valid credentials that + // will not expire soon. + const requestCallback = async () => { + const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user1`, { + encoding: 'utf8' + }); + return { accessToken: token, expiresInSeconds: 300 }; + }; + + const refreshCallback = async () => { + const token = await readFile(`${process.env.OIDC_TOKEN_DIR}/test_user1`, { + encoding: 'utf8' + }); + refreshInvocations++; + return { accessToken: token, expiresInSeconds: 300 }; + }; + + const commandStarted = event => { + if (event.commandName === 'find') { + findStarted++; + } + if (event.commandName === 'saslStart') { + saslStarted++; + } + }; + + const commandSucceeded = event => { + if (event.commandName === 'find') { + findSucceeded++; + } + if (event.commandName === 'saslStart') { + saslSucceeded++; + } + }; + + const commandFailed = event => { + if (event.commandName === 'find') { + findFailed++; + } + }; + + before(function () { + // - Clear the cache + cache.clear(); + // - Create a client with the callbacks and an event listener capable of + // listening for SASL commands + client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + REQUEST_TOKEN_CALLBACK: requestCallback, + REFRESH_TOKEN_CALLBACK: refreshCallback + }, + monitorCommands: true + }); + client.on('commandStarted', commandStarted); + client.on('commandSucceeded', commandSucceeded); + client.on('commandFailed', commandFailed); + collection = client.db('test').collection('test'); + }); + + after(async function () { + client.removeAllListeners('commandStarted'); + client.removeAllListeners('commandSucceeded'); + client.removeAllListeners('commandFailed'); + cache.clear(); + await client?.close(); + }); + + context('on the first find invokation', function () { + before(function () { + findStarted = 0; + findSucceeded = 0; + findFailed = 0; + refreshInvocations = 0; + saslStarted = 0; + saslSucceeded = 0; + }); + + // - Perform a find operation. + // - Assert that the refresh callback has not been called. + it('does not call the refresh callback', async function () { + await collection.findOne(); + expect(refreshInvocations).to.equal(0); + }); + }); + + context('when a command errors and needs reauthentication', function () { + // Force a reauthenication using a failCommand of the form: + before(async function () { + findStarted = 0; + findSucceeded = 0; + findFailed = 0; + refreshInvocations = 0; + saslStarted = 0; + saslSucceeded = 0; + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: { times: 1 }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + // Perform another find operation. + await collection.findOne(); + }); + + after(async function () { + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + cache.clear(); + await client?.close(); + }); + + // - Assert that the refresh callback has been called, if possible. + it('calls the refresh callback', function () { + expect(refreshInvocations).to.equal(1); + }); + + // - Assert that a find operation was started twice and a saslStart operation + // was started once during the command execution. + it('starts the find operation twice', function () { + expect(findStarted).to.equal(2); + }); + + it('starts saslStart once', function () { + expect(saslStarted).to.equal(1); + }); + + // - Assert that a find operation succeeeded once and the saslStart operation + // succeeded during the command execution. + it('succeeds on the find once', function () { + expect(findSucceeded).to.equal(1); + }); + + it('succeeds on saslStart once', function () { + expect(saslSucceeded).to.equal(1); + }); + + // Assert that a find operation failed once during the command execution. + it('fails on the find once', function () { + expect(findFailed).to.equal(1); + }); + }); + }); }); }); diff --git a/test/spec/auth/unified/reauthenticate_with_retry.json b/test/spec/auth/unified/reauthenticate_with_retry.json new file mode 100644 index 00000000000..ef110562ede --- /dev/null +++ b/test/spec/auth/unified/reauthenticate_with_retry.json @@ -0,0 +1,191 @@ +{ + "description": "reauthenticate_with_retry", + "schemaVersion": "1.12", + "runOnRequirements": [ + { + "minServerVersion": "6.3", + "auth": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "uriOptions": { + "retryReads": true, + "retryWrites": true + }, + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "db" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collName" + } + } + ], + "initialData": [ + { + "collectionName": "collName", + "databaseName": "db", + "documents": [] + } + ], + "tests": [ + { + "description": "Read command should reauthenticate when receive ReauthenticationRequired error code and retryReads=true", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "find" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "find", + "arguments": { + "filter": {} + }, + "object": "collection0", + "expectResult": [] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=true", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/auth/unified/reauthenticate_with_retry.yml b/test/spec/auth/unified/reauthenticate_with_retry.yml new file mode 100644 index 00000000000..bf7cb56f3c8 --- /dev/null +++ b/test/spec/auth/unified/reauthenticate_with_retry.yml @@ -0,0 +1,104 @@ +--- +description: reauthenticate_with_retry +schemaVersion: '1.12' +runOnRequirements: +- minServerVersion: '6.3' + auth: true +createEntities: +- client: + id: client0 + uriOptions: + retryReads: true + retryWrites: true + observeEvents: + - commandStartedEvent + - commandSucceededEvent + - commandFailedEvent +- database: + id: database0 + client: client0 + databaseName: db +- collection: + id: collection0 + database: database0 + collectionName: collName +initialData: +- collectionName: collName + databaseName: db + documents: [] +tests: +- description: Read command should reauthenticate when receive ReauthenticationRequired + error code and retryReads=true + operations: + - name: failPoint + object: testRunner + arguments: + client: client0 + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - find + errorCode: 391 + - name: find + arguments: + filter: {} + object: collection0 + expectResult: [] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collName + filter: {} + - commandFailedEvent: + commandName: find + - commandStartedEvent: + command: + find: collName + filter: {} + - commandSucceededEvent: + commandName: find +- description: Write command should reauthenticate when receive ReauthenticationRequired + error code and retryWrites=true + operations: + - name: failPoint + object: testRunner + arguments: + client: client0 + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - insert + errorCode: 391 + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandFailedEvent: + commandName: insert + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert diff --git a/test/spec/auth/unified/reauthenticate_without_retry.json b/test/spec/auth/unified/reauthenticate_without_retry.json new file mode 100644 index 00000000000..6fded476344 --- /dev/null +++ b/test/spec/auth/unified/reauthenticate_without_retry.json @@ -0,0 +1,191 @@ +{ + "description": "reauthenticate_without_retry", + "schemaVersion": "1.12", + "runOnRequirements": [ + { + "minServerVersion": "6.3", + "auth": true + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "uriOptions": { + "retryReads": false, + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "db" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collName" + } + } + ], + "initialData": [ + { + "collectionName": "collName", + "databaseName": "db", + "documents": [] + } + ], + "tests": [ + { + "description": "Read command should reauthenticate when receive ReauthenticationRequired error code and retryReads=false", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "find" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "find", + "arguments": { + "filter": {} + }, + "object": "collection0", + "expectResult": [] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=false", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/auth/unified/reauthenticate_without_retry.yml b/test/spec/auth/unified/reauthenticate_without_retry.yml new file mode 100644 index 00000000000..394c4be91e0 --- /dev/null +++ b/test/spec/auth/unified/reauthenticate_without_retry.yml @@ -0,0 +1,104 @@ +--- +description: reauthenticate_without_retry +schemaVersion: '1.13' +runOnRequirements: +- minServerVersion: '6.3' + auth: true +createEntities: +- client: + id: client0 + uriOptions: + retryReads: false + retryWrites: false + observeEvents: + - commandStartedEvent + - commandSucceededEvent + - commandFailedEvent +- database: + id: database0 + client: client0 + databaseName: db +- collection: + id: collection0 + database: database0 + collectionName: collName +initialData: +- collectionName: collName + databaseName: db + documents: [] +tests: +- description: Read command should reauthenticate when receive ReauthenticationRequired + error code and retryReads=false + operations: + - name: failPoint + object: testRunner + arguments: + client: client0 + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - find + errorCode: 391 + - name: find + arguments: + filter: {} + object: collection0 + expectResult: [] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collName + filter: {} + - commandFailedEvent: + commandName: find + - commandStartedEvent: + command: + find: collName + filter: {} + - commandSucceededEvent: + commandName: find +- description: Write command should reauthenticate when receive ReauthenticationRequired + error code and retryWrites=false + operations: + - name: failPoint + object: testRunner + arguments: + client: client0 + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - insert + errorCode: 391 + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandFailedEvent: + commandName: insert + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert diff --git a/test/unit/cmap/auth/auth_provider.test.ts b/test/unit/cmap/auth/auth_provider.test.ts new file mode 100644 index 00000000000..96d49c650c3 --- /dev/null +++ b/test/unit/cmap/auth/auth_provider.test.ts @@ -0,0 +1,20 @@ +import { expect } from 'chai'; + +import { AuthProvider, MongoRuntimeError } from '../../../mongodb'; + +describe('AuthProvider', function () { + describe('#reauth', function () { + context('when the provider is already reauthenticating', function () { + const provider = new AuthProvider(); + const context = { reauthenticating: true }; + + it('returns an error', function () { + provider.reauth(context, error => { + expect(error).to.exist; + expect(error).to.be.instanceOf(MongoRuntimeError); + expect(error?.message).to.equal('Reauthentication already in progress.'); + }); + }); + }); + }); +}); diff --git a/test/unit/cmap/connection_pool.test.js b/test/unit/cmap/connection_pool.test.js index 62a9e04b5a9..28d88379b81 100644 --- a/test/unit/cmap/connection_pool.test.js +++ b/test/unit/cmap/connection_pool.test.js @@ -3,7 +3,6 @@ const { ConnectionPool } = require('../../mongodb'); const { WaitQueueTimeoutError } = require('../../mongodb'); const mock = require('../../tools/mongodb-mock/index'); -const cmapEvents = require('../../mongodb'); const sinon = require('sinon'); const { expect } = require('chai'); const { setImmediate } = require('timers'); @@ -308,36 +307,5 @@ describe('Connection Pool', function () { callback ); }); - - it('should still manage a connection if no callback is provided', function (done) { - server.setMessageHandler(request => { - const doc = request.document; - if (isHello(doc)) { - request.reply(mock.HELLO); - } - }); - - const pool = new ConnectionPool(server, { - maxPoolSize: 1, - hostAddress: server.hostAddress() - }); - pool.ready(); - - const events = []; - pool.on('connectionCheckedOut', event => events.push(event)); - pool.on('connectionCheckedIn', event => { - events.push(event); - - expect(events).to.have.length(2); - expect(events[0]).to.be.instanceOf(cmapEvents.ConnectionCheckedOutEvent); - expect(events[1]).to.be.instanceOf(cmapEvents.ConnectionCheckedInEvent); - pool.close(done); - }); - - pool.withConnection(undefined, (err, conn, cb) => { - expect(err).to.not.exist; - cb(); - }); - }); }); });