From dbdd89988de25a9fc8b28491e411b320ef768882 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 27 Feb 2023 19:34:34 +0100 Subject: [PATCH] feat(NODE-5036): reauthenticate OIDC and retry --- src/cmap/auth/auth_provider.ts | 8 +- src/cmap/auth/mongodb_oidc.ts | 4 +- .../auth/mongodb_oidc/callback_workflow.ts | 10 +- src/cmap/auth/mongodb_oidc/workflow.ts | 6 +- src/cmap/auth/scram.ts | 4 +- src/cmap/connect.ts | 4 +- src/cmap/connection.ts | 4 +- src/cmap/connection_pool.ts | 72 ++++++- src/error.ts | 3 +- src/index.ts | 1 + test/integration/auth/auth.spec.test.ts | 8 + test/manual/mongodb_oidc.prose.test.ts | 148 ++++++++++++++ .../unified/reauthenticate_with_retry.json | 191 ++++++++++++++++++ .../unified/reauthenticate_with_retry.yml | 104 ++++++++++ .../unified/reauthenticate_without_retry.json | 191 ++++++++++++++++++ .../unified/reauthenticate_without_retry.yml | 104 ++++++++++ test/unit/index.test.ts | 2 + 17 files changed, 849 insertions(+), 15 deletions(-) create mode 100644 test/integration/auth/auth.spec.test.ts create mode 100644 test/spec/auth/unified/reauthenticate_with_retry.json create mode 100644 test/spec/auth/unified/reauthenticate_with_retry.yml create mode 100644 test/spec/auth/unified/reauthenticate_without_retry.json create mode 100644 test/spec/auth/unified/reauthenticate_without_retry.yml diff --git a/src/cmap/auth/auth_provider.ts b/src/cmap/auth/auth_provider.ts index 2a38abe9b4..26f90dd779 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 if for reauthentication. */ + reauthenticating = false; /** The options passed to the `connect` method */ options: AuthContextOptions; diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index be06df6d9e..8f5f4af5b7 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -65,7 +65,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 +86,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 8b2da5ad79..9bdec36535 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 8b88ee00a8..68d0b97688 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/scram.ts b/src/cmap/auth/scram.ts index 7a339151fa..dbe4ce25a3 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 a51c7dd69e..adefc25f73 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 c366c7c567..708df07e9b 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, @@ -127,7 +128,6 @@ export interface ConnectionOptions noDelay?: boolean; socketTimeoutMS?: number; cancellationToken?: CancellationToken; - metadata: ClientMetadata; } @@ -165,6 +165,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 ca3270db5c..1ce33697d4 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -16,8 +16,10 @@ import { CONNECTION_READY } from '../constants'; import { + MONGODB_ERROR_CODES, MongoError, MongoInvalidArgumentError, + MongoMissingCredentialsError, MongoNetworkError, MongoRuntimeError, MongoServerError @@ -25,7 +27,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, @@ -544,7 +546,17 @@ export class ConnectionPool extends TypedEventEmitter { fn(undefined, conn, (fnErr, result) => { if (typeof callback === 'function') { if (fnErr) { - callback(fnErr); + if ((fnErr as MongoError).code === MONGODB_ERROR_CODES.Reauthenticate) { + this.reauthenticate(conn, fn, (error, res) => { + if (error) { + callback(error); + } else { + callback(undefined, res); + } + }); + } else { + callback(fnErr); + } } else { callback(undefined, result); } @@ -559,7 +571,17 @@ export class ConnectionPool extends TypedEventEmitter { fn(err as MongoError, conn, (fnErr, result) => { if (typeof callback === 'function') { if (fnErr) { - callback(fnErr); + if (conn && (fnErr as MongoError).code === MONGODB_ERROR_CODES.Reauthenticate) { + this.reauthenticate(conn, fn, (error, res) => { + if (error) { + callback(error); + } else { + callback(undefined, res); + } + }); + } else { + callback(fnErr); + } } else { callback(undefined, result); } @@ -572,6 +594,50 @@ export class ConnectionPool extends TypedEventEmitter { }); } + /** + * 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.')); + } + authContext.reauthenticating = true; + 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.auth(authContext, error => { + authContext.reauthenticating = false; + 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 1dd426cb4f..9f909289ce 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 22ab7f4b99..1bd5e812c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -197,6 +197,7 @@ export type { ResumeToken, UpdateDescription } from './change_stream'; +export { 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 0000000000..cfce338e8d --- /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 f89eec1001..b5665d7b51 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -452,5 +452,153 @@ describe('MONGODB-OIDC', function () { }); }); }); + + // The driver MUST test reauthentication with MONGODB-OIDC for a read operation. + describe('6. Reauthentication', function () { + let refreshInvokations = 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' + }); + refreshInvokations++; + 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 + // + // - TODO(NODE-3494): Driver does not observe sensitive 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 () { + await client?.close(); + }); + + context('on the first find invokation', function () { + before(function () { + findStarted = 0; + findSucceeded = 0; + findFailed = 0; + refreshInvokations = 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(refreshInvokations).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; + refreshInvokations = 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(); + }); + + // - Assert that the refresh callback has been called, if possible. + it('calls the refresh callback', function () { + expect(refreshInvokations).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 0000000000..ef110562ed --- /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 0000000000..bf7cb56f3c --- /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 0000000000..6fded47634 --- /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 0000000000..394c4be91e --- /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/index.test.ts b/test/unit/index.test.ts index 833082b983..77d40a4d52 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -16,6 +16,8 @@ const EXPECTED_EXPORTS = [ 'AbstractCursor', 'Admin', 'AggregationCursor', + 'AuthContext', + 'AuthContextOptions', 'AuthMechanism', 'AutoEncryptionLoggerLevel', 'BatchType',