From 88398b1228254ec7a25653f9efc5ca2d8932f033 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 14 Dec 2022 11:26:10 +0100 Subject: [PATCH 01/70] Add AuthTokenProvider configuration --- .../connection-provider-direct.js | 4 +- .../connection-provider-pooled.js | 39 ++++++++++++++----- .../connection-provider-routing.js | 4 +- packages/core/src/types.ts | 9 +++++ packages/neo4j-driver-lite/src/index.ts | 32 +++++++++++---- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index b4cb1ce61..129acec41 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -32,8 +32,8 @@ const { const { SERVICE_UNAVAILABLE } = error export default class DirectConnectionProvider extends PooledConnectionProvider { - constructor ({ id, config, log, address, userAgent, authToken }) { - super({ id, config, log, userAgent, authToken }) + constructor ({ id, config, log, address, userAgent, authTokenProvider }) { + super({ id, config, log, userAgent, authTokenProvider }) this._address = address } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index f6424d5ba..da03184ac 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -24,7 +24,7 @@ import { error, ConnectionProvider, ServerInfo } from 'neo4j-driver-core' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { constructor ( - { id, config, log, userAgent, authToken }, + { id, config, log, userAgent, authTokenProvider }, createChannelConnectionHook = null ) { super() @@ -33,7 +33,8 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._config = config this._log = log this._userAgent = userAgent - this._authToken = authToken + this._renewableAuthToken = undefined + this._authTokenProvider = authTokenProvider this._createChannelConnection = createChannelConnectionHook || (address => { @@ -75,17 +76,35 @@ export default class PooledConnectionProvider extends ConnectionProvider { return release(address, connection) } this._openConnections[connection.id] = connection - return connection - .connect(this._userAgent, this._authToken) - .catch(error => { - // let's destroy this connection - this._destroyConnection(connection) - // propagate the error because connection failed to connect / initialize - throw error - }) + return this._getAuthToken() + .then(authToken => connection + .connect(this._userAgent, authToken) + .catch(error => { + // let's destroy this connection + this._destroyConnection(connection) + // propagate the error because connection failed to connect / initialize + throw error + })) }) } + async _getAuthToken () { + if (!this._isRenewableTokenAcquired() || this._isRenewableTokenExpired()) { + this._renewableAuthToken = await this._authTokenProvider() + } + + return this._renewableAuthToken + } + + _isRenewableTokenAcquired () { + return !!this._renewableAuthToken + } + + _isRenewableTokenExpired () { + return this._renewableAuthToken.expectedExpirationTime && + this._renewableAuthToken.expectedExpirationTime < new Date() + } + /** * Check that a connection is usable * @return {boolean} true if the connection is open diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index f0442ab23..3cd1d1c0e 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -65,10 +65,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider config, log, userAgent, - authToken, + authTokenProvider, routingTablePurgeDelay }) { - super({ id, config, log, userAgent, authToken }, address => { + super({ id, config, log, userAgent, authTokenProvider }, address => { return createChannelConnection( address, this._config, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 63140cc16..ccd93ead2 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -48,6 +48,15 @@ export interface AuthToken { realm?: string parameters?: Parameters } + +export interface RenewableAuthToken { + expectedExpirationTime?: Date + authToken: AuthToken +} + +// Can be async, the user probably wants to do some IO. +export type AuthTokenProvider = () => Promise | RenewableAuthToken + export interface Config { encrypted?: boolean | EncryptionLevel trust?: TrustStrategy diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index e64639855..b5218a3b1 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -101,6 +101,8 @@ import { } from 'neo4j-driver-bolt-connection' type AuthToken = coreTypes.AuthToken +type RenewableAuthToken = coreTypes.RenewableAuthToken +type AuthTokenProvider = coreTypes.AuthTokenProvider type Config = coreTypes.Config type TrustStrategy = coreTypes.TrustStrategy type EncryptionLevel = coreTypes.EncryptionLevel @@ -116,6 +118,22 @@ const { urlUtil } = internal +function createAuthProvider (authTokenOrProvider: AuthToken | AuthTokenProvider): AuthTokenProvider { + if (typeof authTokenOrProvider === 'function') { + return authTokenOrProvider + } + + let authToken: AuthToken = authTokenOrProvider + // Sanitize authority token. Nicer error from server when a scheme is set. + authToken = authToken ?? {} + authToken.scheme = authToken.scheme ?? 'none' + return function (): RenewableAuthToken { + return { + authToken + } + } +} + /** * Construct a new Neo4j Driver. This is your main entry point for this * library. @@ -246,13 +264,13 @@ const { * } * * @param {string} url The URL for the Neo4j database, for instance "neo4j://localhost" and/or "bolt://localhost" - * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. + * @param {Map| function()} authToken Authentication credentials. See {@link auth} for helpers. * @param {Object} config Configuration object. See the configuration section above for details. * @returns {Driver} */ function driver ( url: string, - authToken: AuthToken, + authToken: AuthToken | AuthTokenProvider, config: Config = {} ): Driver { assertString(url, 'Bolt URL') @@ -302,9 +320,7 @@ function driver ( config.trust = trust } - // Sanitize authority token. Nicer error from server when a scheme is set. - authToken = authToken ?? {} - authToken.scheme = authToken.scheme ?? 'none' + const authTokenProvider = createAuthProvider(authToken) // Use default user agent or user agent specified by user. config.userAgent = config.userAgent ?? USER_AGENT @@ -331,7 +347,7 @@ function driver ( config, log, hostNameResolver, - authToken, + authTokenProvider, address, userAgent: config.userAgent, routingContext: parsedUrl.query @@ -348,7 +364,7 @@ function driver ( id, config, log, - authToken, + authTokenProvider, address, userAgent: config.userAgent }) @@ -585,6 +601,8 @@ export { export type { QueryResult, AuthToken, + AuthTokenProvider, + RenewableAuthToken, Config, EncryptionLevel, TrustStrategy, From 4fb19db63eecb941c95e143621973214c71d58de Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 14 Dec 2022 16:46:59 +0100 Subject: [PATCH 02/70] Handling auth error --- .../connection-provider-direct.js | 13 ++-- .../connection-provider-pooled.js | 62 +++++++++++++++++-- .../connection-provider-routing.js | 16 +++-- .../src/connection/connection-channel.js | 23 ++++++- .../src/connection/connection-delegate.js | 16 +++++ .../connection/connection-error-handler.js | 8 +-- .../src/connection/connection.js | 14 ++++- packages/bolt-connection/src/pool/pool.js | 38 +++++++++++- packages/core/src/connection.ts | 8 +++ 9 files changed, 176 insertions(+), 22 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 129acec41..c12c15b91 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -45,8 +45,8 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { acquireConnection ({ accessMode, database, bookmarks } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, - handleAuthorizationExpired: (error, address) => - this._handleAuthorizationExpired(error, address, database) + handleAuthorizationExpired: (error, address, conn) => + this._handleAuthorizationExpired(error, address, conn, database) }) return this._connectionPool @@ -57,11 +57,16 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } - _handleAuthorizationExpired (error, address, database) { + _handleAuthorizationExpired (error, address, conn, database) { this._log.warn( `Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - this._connectionPool.purge(address).catch(() => {}) + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { + this._connectionPool.apply(address, (conn) => conn.authToken === null) + } else { + this._refreshToken() + } + return error } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index da03184ac..eea52e2b2 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -90,10 +90,48 @@ export default class PooledConnectionProvider extends ConnectionProvider { async _getAuthToken () { if (!this._isRenewableTokenAcquired() || this._isRenewableTokenExpired()) { - this._renewableAuthToken = await this._authTokenProvider() + this._refreshToken() } - return this._renewableAuthToken + if (this._refreshObserver) { + const promiseState = {} + const promise = new Promise((resolve, reject) => { + promiseState.resolve = resolve + promiseState.reject = reject + }) + + this._refreshObserver.subscribe({ + onSuccess: promiseState.resolve, + onError: promiseState.onError + }) + + await promise + } + + return this._renewableAuthToken.authToken + } + + _refreshToken () { + if (!this._refreshObserver) { + const subscribers = [] + this._refreshObserver = { + subscribe: (sub) => subscribers.push(sub), + notify: () => subscribers.forEach(sub => sub.onSuccess()), + notifyError: (e) => subscribers.forEach(sub => sub.onError(e)) + } + Promise.resolve(this._authTokenProvider()) + .then(token => { + this._renewableAuthToken = token + this._refreshObserver.notify() + return token + }) + .catch(e => { + this._refreshObserver.notifyError(e) + }) + .finally(() => { + this._refreshObserver = undefined + }) + } } _isRenewableTokenAcquired () { @@ -110,14 +148,30 @@ export default class PooledConnectionProvider extends ConnectionProvider { * @return {boolean} true if the connection is open * @access private **/ - _validateConnection (conn) { + async _validateConnection (conn) { if (!conn.isOpen()) { return false } const maxConnectionLifetime = this._config.maxConnectionLifetime const lifetime = Date.now() - conn.creationTimestamp - return lifetime <= maxConnectionLifetime + if (lifetime > maxConnectionLifetime) { + return false + } + + if (this._renewableAuthToken.authToken !== conn.authToken || this._isRenewableTokenExpired()) { + if (!conn.supportsReAuth) { + return false + } + try { + const authToken = await this._getAuthToken() + await conn.reAuth(authToken) + } catch (e) { + return false + } + } + + return true } /** diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 3cd1d1c0e..274285f1c 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -109,11 +109,17 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return error } - _handleAuthorizationExpired (error, address, database) { + _handleAuthorizationExpired (error, address, conn, database) { this._log.warn( `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - this._connectionPool.purge(address).catch(() => {}) + + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { + this._connectionPool.apply(address, (conn) => conn.authToken === null) + } else { + this._refreshToken() + } + return error } @@ -142,8 +148,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider SESSION_EXPIRED, (error, address) => this._handleUnavailability(error, address, context.database), (error, address) => this._handleWriteFailure(error, address, context.database), - (error, address) => - this._handleAuthorizationExpired(error, address, context.database) + (error, address, conn) => + this._handleAuthorizationExpired(error, address, conn, context.database) ) const routingTable = await this._freshRoutingTable({ @@ -522,7 +528,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SESSION_EXPIRED, - handleAuthorizationExpired: (error, address) => this._handleAuthorizationExpired(error, address) + handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn) }) const connectionProvider = new SingleConnectionProvider( diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 8b270519f..ed8413c77 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -124,7 +124,7 @@ export default class ChannelConnection extends Connection { protocolSupplier ) { super(errorHandler) - + this._authToken = null this._reseting = false this._resetObservers = [] this._id = idGenerator++ @@ -148,6 +148,8 @@ export default class ChannelConnection extends Connection { */ this._protocol = protocolSupplier(this) + this._supportsReAuth = this._protocol.version > 5.0 + // Set to true on fatal errors, to get this out of connection pool. this._isBroken = false @@ -156,6 +158,18 @@ export default class ChannelConnection extends Connection { } } + get authToken () { + return this._authToken + } + + set authToken (value) { + this._authToken = value + } + + get supportsReAuth () { + return this._supportsReAuth + } + get id () { return this._id } @@ -175,9 +189,16 @@ export default class ChannelConnection extends Connection { * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ connect (userAgent, authToken) { + this._authToken = authToken return this._initialize(userAgent, authToken) } + async reAuth (authToken) { + this._authToken = authToken + console.log('re aut') + return this + } + /** * Perform protocol-specific initialization which includes authentication. * @param {string} userAgent the user agent for this driver. diff --git a/packages/bolt-connection/src/connection/connection-delegate.js b/packages/bolt-connection/src/connection/connection-delegate.js index 16d2da48a..925858102 100644 --- a/packages/bolt-connection/src/connection/connection-delegate.js +++ b/packages/bolt-connection/src/connection/connection-delegate.js @@ -51,6 +51,18 @@ export default class DelegateConnection extends Connection { return this._delegate.server } + get authToken () { + return this._delegate.authToken + } + + get supportsReAuth () { + return this._delegate.supportsReAuth + } + + set authToken (value) { + this._delegate.authToken = value + } + get address () { return this._delegate.address } @@ -67,6 +79,10 @@ export default class DelegateConnection extends Connection { return this._delegate.isOpen() } + reAuth (authToken) { + return this._delegate.reAuth(authToken) + } + protocol () { return this._delegate.protocol() } diff --git a/packages/bolt-connection/src/connection/connection-error-handler.js b/packages/bolt-connection/src/connection/connection-error-handler.js index de844e96f..91f855c11 100644 --- a/packages/bolt-connection/src/connection/connection-error-handler.js +++ b/packages/bolt-connection/src/connection/connection-error-handler.js @@ -62,15 +62,15 @@ export default class ConnectionErrorHandler { * @param {ServerAddress} address the address of the connection where the error happened. * @return {Neo4jError} new error that should be propagated to the user. */ - handleAndTransformError (error, address) { + handleAndTransformError (error, address, connection) { if (isAutorizationExpiredError(error)) { - return this._handleAuthorizationExpired(error, address) + return this._handleAuthorizationExpired(error, address, connection) } if (isAvailabilityError(error)) { - return this._handleUnavailability(error, address) + return this._handleUnavailability(error, address, connection) } if (isFailureToWrite(error)) { - return this._handleWriteFailure(error, address) + return this._handleWriteFailure(error, address, connection) } return error } diff --git a/packages/bolt-connection/src/connection/connection.js b/packages/bolt-connection/src/connection/connection.js index d3c692712..9d1107023 100644 --- a/packages/bolt-connection/src/connection/connection.js +++ b/packages/bolt-connection/src/connection/connection.js @@ -39,6 +39,18 @@ export default class Connection { throw new Error('not implemented') } + get authToken () { + throw new Error('not implemented') + } + + set authToken (value) { + throw new Error('not implemented') + } + + get supportsReAuth () { + throw new Error('not implemented') + } + /** * @returns {boolean} whether this connection is in a working condition */ @@ -124,7 +136,7 @@ export default class Connection { */ handleAndTransformError (error, address) { if (this._errorHandler) { - return this._errorHandler.handleAndTransformError(error, address) + return this._errorHandler.handleAndTransformError(error, address, this) } return error diff --git a/packages/bolt-connection/src/pool/pool.js b/packages/bolt-connection/src/pool/pool.js index d23f8f8cf..3f7199589 100644 --- a/packages/bolt-connection/src/pool/pool.js +++ b/packages/bolt-connection/src/pool/pool.js @@ -123,6 +123,14 @@ class Pool { return this._purgeKey(address.asKey()) } + apply (address, resourceConsumer) { + const key = address.asKey() + + if (key in this._pools) { + this._pools[key].apply(resourceConsumer) + } + } + /** * Destroy all idle resources in this pool. * @returns {Promise} A promise that is resolved when the resources are purged @@ -195,7 +203,7 @@ class Pool { while (pool.length) { const resource = pool.pop() - if (this._validate(resource)) { + if (await this._validate(resource)) { if (this._removeIdleObserver) { this._removeIdleObserver(resource) } @@ -207,6 +215,7 @@ class Pool { } return { resource, pool } } else { + pool.removeInUse(resource) await this._destroy(resource) } } @@ -231,6 +240,7 @@ class Pool { // Invoke callback that creates actual connection resource = await this._create(address, (address, resource) => this._release(address, resource, pool)) + pool.pushInUse(resource) resourceAcquired(key, this._activeResourceCounts) if (this._log.isDebugEnabled()) { this._log.debug(`${resource} created for the pool ${key}`) @@ -246,12 +256,13 @@ class Pool { if (pool.isActive()) { // there exist idle connections for the given key - if (!this._validate(resource)) { + if (!await this._validate(resource)) { if (this._log.isDebugEnabled()) { this._log.debug( `${resource} destroyed and can't be released to the pool ${key} because it is not functional` ) } + pool.removeInUse(resource) await this._destroy(resource) } else { if (this._installIdleObserver) { @@ -263,6 +274,7 @@ class Pool { const pool = this._pools[key] if (pool) { this._pools[key] = pool.filter(r => r !== resource) + pool.removeInUse(resource) } // let's not care about background clean-ups due to errors but just trigger the destroy // process for the resource, we especially catch any errors and ignore them to avoid @@ -283,6 +295,7 @@ class Pool { `${resource} destroyed and can't be released to the pool ${key} because pool has been purged` ) } + pool.removeInUse(resource) await this._destroy(resource) } resourceReleased(key, this._activeResourceCounts) @@ -419,6 +432,7 @@ class SingleAddressPool { constructor () { this._active = true this._elements = [] + this._elementsInUse = new Set() } isActive () { @@ -427,6 +441,8 @@ class SingleAddressPool { close () { this._active = false + this._elements = [] + this._elementsInUse = new Set() } filter (predicate) { @@ -434,17 +450,33 @@ class SingleAddressPool { return this } + apply (resourceConsumer) { + this._elements.forEach(resourceConsumer) + this._elementsInUse.forEach(resourceConsumer) + } + get length () { return this._elements.length } pop () { - return this._elements.pop() + const element = this._elements.pop() + this._elementsInUse.add(element) + return element } push (element) { + this._elementsInUse.delete(element) return this._elements.push(element) } + + pushInUse (element) { + this._elementsInUse.add(element) + } + + removeInUse (element) { + this._elementsInUse.delete(element) + } } export default Pool diff --git a/packages/core/src/connection.ts b/packages/core/src/connection.ts index 6761adc58..eba5b2e98 100644 --- a/packages/core/src/connection.ts +++ b/packages/core/src/connection.ts @@ -37,6 +37,10 @@ class Connection { return {} } + get authToken (): any { + return {} + } + /** * @property {ServerAddress} the server address this connection is opened against */ @@ -51,6 +55,10 @@ class Connection { return undefined } + get supportsReAuth (): boolean { + return false + } + /** * @returns {boolean} whether this connection is in a working condition */ From 89b0b44cfff7c35b977ccf6259a56543c3ec4f50 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 14 Dec 2022 18:16:45 +0100 Subject: [PATCH 03/70] Add support for impersonation --- .../connection-provider-routing.js | 90 ++++++++++++++----- packages/core/src/connection-provider.ts | 2 + packages/core/src/driver.ts | 20 +++-- .../core/src/internal/connection-holder.ts | 17 +++- packages/core/src/session.ts | 8 +- 5 files changed, 107 insertions(+), 30 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 274285f1c..c26aaf96e 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -139,7 +139,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { let name let address const context = { database: database || DEFAULT_DB_NAME } @@ -157,6 +157,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider database: context.database, bookmarks, impersonatedUser, + auth, onDatabaseNameResolved: (databaseName) => { context.database = context.database || databaseName if (onDatabaseNameResolved) { @@ -191,6 +192,17 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider routingTable ) + if (auth && auth !== connection.authToken) { + console.log('need to re-auth') + if (connection.supportsReAuth) { + connection.reAuth(auth, true) + } else { + await connection._release() + console.log('create sticky connection') + return await this._createStickyConnection({ address, auth }) + } + } + return new DelegateConnection(connection, databaseSpecificErrorHandler) } catch (error) { const transformed = databaseSpecificErrorHandler.handleAndTransformError( @@ -310,7 +322,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return this._connectionPool.acquire(address) } - _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved } = {}) { + _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { const currentRoutingTable = this._routingTableRegistry.get( database, () => new RoutingTable({ database }) @@ -322,10 +334,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._log.info( `Routing table is stale for database: "${database}" and access mode: "${accessMode}": ${currentRoutingTable}` ) - return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved) + return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) } - _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved) { + _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) { const knownRouters = currentRoutingTable.routers if (this._useSeedRouter) { @@ -334,7 +346,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - onDatabaseNameResolved + onDatabaseNameResolved, + auth ) } return this._fetchRoutingTableFromKnownRoutersFallbackToSeedRouter( @@ -342,7 +355,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - onDatabaseNameResolved + onDatabaseNameResolved, + auth ) } @@ -351,7 +365,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - onDatabaseNameResolved + onDatabaseNameResolved, + auth ) { // we start with seed router, no routers were probed before const seenRouters = [] @@ -360,7 +375,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._seedRouter, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) if (newRoutingTable) { @@ -371,7 +387,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider knownRouters, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) newRoutingTable = newRoutingTable2 error = error2 || error @@ -390,13 +407,15 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - onDatabaseNameResolved + onDatabaseNameResolved, + auth ) { let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters( knownRouters, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) if (!newRoutingTable) { @@ -406,7 +425,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._seedRouter, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) } @@ -422,13 +442,15 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider knownRouters, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) { const [newRoutingTable, error] = await this._fetchRoutingTable( knownRouters, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) if (newRoutingTable) { @@ -453,7 +475,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider seedRouter, routingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) { const resolvedAddresses = await this._resolveSeedRouter(seedRouter) @@ -462,7 +485,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider address => seenRouters.indexOf(address) < 0 ) - return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser) + return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth) } async _resolveSeedRouter (seedRouter) { @@ -474,7 +497,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return [].concat.apply([], dnsResolvedAddresses) } - async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser) { + async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth) { return routerAddresses.reduce( async (refreshedTablePromise, currentRouter, currentIndex) => { const [newRoutingTable] = await refreshedTablePromise @@ -497,7 +520,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const [session, error] = await this._createSessionForRediscovery( currentRouter, bookmarks, - impersonatedUser + impersonatedUser, + auth ) if (session) { try { @@ -522,9 +546,34 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser) { + async _createStickyConnection ({ address, auth }) { + const connection = await this._createChannelConnection(address) + connection._release = () => this._destroyConnection(connection) + this._openConnections[connection.id] = connection + + try { + return await connection.connect(this._userAgent, auth) + } catch (error) { + await this._destroyConnection() + throw error + } + } + + async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth) { try { - const connection = await this._connectionPool.acquire(routerAddress) + let connection = await this._connectionPool.acquire(routerAddress) + + if (auth && connection.authToken !== auth) { + if (connection.supportsReAuth) { + await connection.reAuth(auth) + } else { + await connection._release() + connection = await this._createStickyConnection({ + address: routerAddress, + auth + }) + } + } const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SESSION_EXPIRED, @@ -720,6 +769,7 @@ function _isFailFastError (error) { } function _isFailFastSecurityError (error) { + console.error(error) return error.code.startsWith('Neo.ClientError.Security.') && ![ AUTHORIZATION_EXPIRED_CODE diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index 8a900a3a9..09057a9d0 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -21,6 +21,7 @@ import Connection from './connection' import { bookmarks } from './internal' import { ServerInfo } from './result-summary' +import { AuthToken } from './types' /** * Inteface define a common way to acquire a connection @@ -51,6 +52,7 @@ class ConnectionProvider { bookmarks: bookmarks.Bookmarks impersonatedUser?: string onDatabaseNameResolved?: (databaseName?: string) => void + auth?: AuthToken }): Promise { throw Error('Not implemented') } diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index a3fcb2dc4..8fdd2392c 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -38,7 +38,8 @@ import { LoggingConfig, TrustStrategy, SessionMode, - Query + Query, + AuthToken } from './types' import { ServerAddress } from './internal/server-address' import BookmarkManager, { bookmarkManager } from './bookmark-manager' @@ -96,6 +97,7 @@ type CreateSession = (args: { impersonatedUser?: string bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter + auth?: AuthToken }) => Session type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor @@ -121,6 +123,7 @@ class SessionConfig { fetchSize?: number bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter + auth?: AuthToken /** * @constructor @@ -696,7 +699,8 @@ class Driver { impersonatedUser, fetchSize, bookmarkManager, - notificationFilter + notificationFilter, + auth }: SessionConfig = {}): Session { return this._newSession({ defaultAccessMode, @@ -707,7 +711,8 @@ class Driver { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize!), bookmarkManager, - notificationFilter + notificationFilter, + auth }) } @@ -746,7 +751,8 @@ class Driver { impersonatedUser, fetchSize, bookmarkManager, - notificationFilter + notificationFilter, + auth }: { defaultAccessMode: SessionMode bookmarkOrBookmarks?: string | string[] @@ -755,7 +761,8 @@ class Driver { impersonatedUser?: string fetchSize: number bookmarkManager?: BookmarkManager - notificationFilter?: NotificationFilter + notificationFilter?: NotificationFilter, + auth?: AuthToken }): Session { const sessionMode = Session._validateSessionMode(defaultAccessMode) const connectionProvider = this._getOrCreateConnectionProvider() @@ -773,7 +780,8 @@ class Driver { impersonatedUser, fetchSize, bookmarkManager, - notificationFilter + notificationFilter, + auth }) } diff --git a/packages/core/src/internal/connection-holder.ts b/packages/core/src/internal/connection-holder.ts index 17b5690d1..787a61f86 100644 --- a/packages/core/src/internal/connection-holder.ts +++ b/packages/core/src/internal/connection-holder.ts @@ -24,6 +24,7 @@ import Connection from '../connection' import { ACCESS_MODE_WRITE } from './constants' import { Bookmarks } from './bookmarks' import ConnectionProvider from '../connection-provider' +import { AuthToken } from '../types' /** * @private @@ -85,6 +86,8 @@ class ConnectionHolder implements ConnectionHolderInterface { private readonly _impersonatedUser?: string private readonly _getConnectionAcquistionBookmarks: () => Promise private readonly _onDatabaseNameResolved?: (databaseName?: string) => void + private readonly _auth?: AuthToken + private _closed: boolean /** * @constructor @@ -104,7 +107,8 @@ class ConnectionHolder implements ConnectionHolderInterface { connectionProvider, impersonatedUser, onDatabaseNameResolved, - getConnectionAcquistionBookmarks + getConnectionAcquistionBookmarks, + auth }: { mode?: string database?: string @@ -113,8 +117,10 @@ class ConnectionHolder implements ConnectionHolderInterface { impersonatedUser?: string onDatabaseNameResolved?: (databaseName?: string) => void getConnectionAcquistionBookmarks?: () => Promise + auth?: AuthToken } = {}) { this._mode = mode + this._closed = false this._database = database != null ? assertString(database, 'database') : '' this._bookmarks = bookmarks ?? Bookmarks.empty() this._connectionProvider = connectionProvider @@ -122,6 +128,7 @@ class ConnectionHolder implements ConnectionHolderInterface { this._referenceCount = 0 this._connectionPromise = Promise.resolve(null) this._onDatabaseNameResolved = onDatabaseNameResolved + this._auth = auth this._getConnectionAcquistionBookmarks = getConnectionAcquistionBookmarks ?? (() => Promise.resolve(Bookmarks.empty())) } @@ -166,7 +173,8 @@ class ConnectionHolder implements ConnectionHolderInterface { database: this._database, bookmarks: await this._getBookmarks(), impersonatedUser: this._impersonatedUser, - onDatabaseNameResolved: this._onDatabaseNameResolved + onDatabaseNameResolved: this._onDatabaseNameResolved, + auth: this._auth }) } @@ -192,6 +200,7 @@ class ConnectionHolder implements ConnectionHolderInterface { } close (hasTx?: boolean): Promise { + this._closed = true if (this._referenceCount === 0) { return this._connectionPromise } @@ -216,6 +225,10 @@ class ConnectionHolder implements ConnectionHolderInterface { .catch(ignoreError) .then(() => connection._release().then(() => null)) } + + if (!this._closed && (this._auth != null) && !connection.supportsReAuth) { + return connection + } return connection._release().then(() => null) } else { return Promise.resolve(null) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index d29509bf9..255ae5301 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -30,7 +30,7 @@ import { TransactionExecutor } from './internal/transaction-executor' import { Bookmarks } from './internal/bookmarks' import { TxConfig } from './internal/tx-config' import ConnectionProvider from './connection-provider' -import { Query, SessionMode } from './types' +import { AuthToken, Query, SessionMode } from './types' import Connection from './connection' import { NumberOrInteger } from './graph-types' import TransactionPromise from './transaction-promise' @@ -98,7 +98,8 @@ class Session { fetchSize, impersonatedUser, bookmarkManager, - notificationFilter + notificationFilter, + auth }: { mode: SessionMode connectionProvider: ConnectionProvider @@ -110,6 +111,7 @@ class Session { impersonatedUser?: string bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter + auth?: AuthToken }) { this._mode = mode this._database = database @@ -119,6 +121,7 @@ class Session { this._getConnectionAcquistionBookmarks = this._getConnectionAcquistionBookmarks.bind(this) this._readConnectionHolder = new ConnectionHolder({ mode: ACCESS_MODE_READ, + auth, database, bookmarks, connectionProvider, @@ -128,6 +131,7 @@ class Session { }) this._writeConnectionHolder = new ConnectionHolder({ mode: ACCESS_MODE_WRITE, + auth, database, bookmarks, connectionProvider, From beac6518c0c35830556046995597a5a851d97349 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 9 Jan 2023 12:17:43 +0100 Subject: [PATCH 04/70] Add holder --- .../src/auth/auth-token-holder.js | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 packages/bolt-connection/src/auth/auth-token-holder.js diff --git a/packages/bolt-connection/src/auth/auth-token-holder.js b/packages/bolt-connection/src/auth/auth-token-holder.js new file mode 100644 index 000000000..7e99bbef6 --- /dev/null +++ b/packages/bolt-connection/src/auth/auth-token-holder.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class AuthTokenHolder { + constructor ({ authTokenProvider }) { + this._getAuthToken = authTokenProvider + this._renewableAuthToken = undefined + this._refreshObserver = undefined + } + + get () { + if (this._renewableAuthToken) { + return this._renewableAuthToken.authToken + } + return undefined + } + + isTokenExpired () { + return !this._renewableAuthToken || + (this._renewableAuthToken.expectedExpirationTime && + this._renewableAuthToken.expectedExpirationTime < new Date()) + } + + async getFresh () { + if (this.isTokenExpired()) { + const promiseState = {} + const promise = new Promise((resolve, reject) => { + promiseState.resolve = resolve + promiseState.reject = reject + }) + + this.scheduleRefresh({ + onSuccess: promiseState.resolve, + onError: promiseState.onError + }) + + await promise + } + + return this.get() + } + + scheduleRefresh (observer) { + // there is no refresh schedule + if (!this._refreshObserver) { + const subscribers = [] + + this._refreshObserver = { + subscribe: (sub) => subscribers.push(sub), + notify: () => subscribers.forEach(sub => sub.onSuccess()), + notifyError: (e) => subscribers.forEach(sub => sub.onError(e)) + } + + Promise.resolve(this._getAuthToken()) + .then(token => { + this._renewableAuthToken = token + this._refreshObserver.notify() + return token + }) + .catch(e => { + this._refreshObserver.notifyError(e) + }) + .finally(() => { + this._refreshObserver = undefined + }) + } + + if (observer) { + this._refreshObserver.subscribe(observer) + } + } +} From 759fec237683d95570809712c3003b054fedf2ac Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 9 Jan 2023 12:59:55 +0100 Subject: [PATCH 05/70] Re-Authentication --- .../authorization-provider.js} | 44 +++++-- .../connection-provider-direct.js | 7 +- .../connection-provider-pooled.js | 93 +++----------- .../connection-provider-routing.js | 7 +- packages/core/src/connection.ts | 6 + .../authorization-provider.js | 116 ++++++++++++++++++ .../connection-provider-direct.js | 18 ++- .../connection-provider-pooled.js | 26 ++-- .../connection-provider-routing.js | 111 +++++++++++++---- .../home-database-provider.js | 32 +++++ .../connection/connection-channel.js | 23 +++- .../connection/connection-delegate.js | 16 +++ .../connection/connection-error-handler.js | 8 +- .../bolt-connection/connection/connection.js | 14 ++- .../lib/bolt-connection/pool/pool.js | 38 +++++- .../lib/core/connection-provider.ts | 2 + .../neo4j-driver-deno/lib/core/connection.ts | 14 +++ packages/neo4j-driver-deno/lib/core/driver.ts | 20 ++- .../lib/core/internal/connection-holder.ts | 17 ++- .../neo4j-driver-deno/lib/core/session.ts | 8 +- packages/neo4j-driver-deno/lib/core/types.ts | 9 ++ packages/neo4j-driver-deno/lib/mod.ts | 32 +++-- 22 files changed, 504 insertions(+), 157 deletions(-) rename packages/bolt-connection/src/{auth/auth-token-holder.js => connection-provider/authorization-provider.js} (69%) create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/home-database-provider.js diff --git a/packages/bolt-connection/src/auth/auth-token-holder.js b/packages/bolt-connection/src/connection-provider/authorization-provider.js similarity index 69% rename from packages/bolt-connection/src/auth/auth-token-holder.js rename to packages/bolt-connection/src/connection-provider/authorization-provider.js index 7e99bbef6..1b33f9161 100644 --- a/packages/bolt-connection/src/auth/auth-token-holder.js +++ b/packages/bolt-connection/src/connection-provider/authorization-provider.js @@ -17,28 +17,56 @@ * limitations under the License. */ -export class AuthTokenHolder { - constructor ({ authTokenProvider }) { +/** + * Class which provides Authorization for {@link Connection} + */ +export default class AuthenticationProvider { + constructor ({ authTokenProvider, userAgent }) { this._getAuthToken = authTokenProvider this._renewableAuthToken = undefined + this._userAgent = userAgent this._refreshObserver = undefined } - get () { + async authenticate ({ connection }) { + if (!this._authToken) { + await this._getFreshAuthToken() + } + + if (this._renewableAuthToken.authToken !== connection.authToken || this._isTokenExpired()) { + return await connection.connect(this._userAgent, this._authToken) + } + + return connection + } + + async handleError ({ connection, code }) { + if ( + connection.authToken === this._authToken && + [ + 'Neo.ClientError.Security.Unauthorized', + 'Neo.ClientError.Security.TokenExpired' + ].includes(code) + ) { + this._scheduleRefresh() + } + } + + get _authToken () { if (this._renewableAuthToken) { return this._renewableAuthToken.authToken } return undefined } - isTokenExpired () { + get _isTokenExpired () { return !this._renewableAuthToken || (this._renewableAuthToken.expectedExpirationTime && this._renewableAuthToken.expectedExpirationTime < new Date()) } - async getFresh () { - if (this.isTokenExpired()) { + async _getFreshAuthToken () { + if (this._isTokenExpired) { const promiseState = {} const promise = new Promise((resolve, reject) => { promiseState.resolve = resolve @@ -53,10 +81,10 @@ export class AuthTokenHolder { await promise } - return this.get() + return this._authToken } - scheduleRefresh (observer) { + _scheduleRefresh (observer) { // there is no refresh schedule if (!this._refreshObserver) { const subscribers = [] diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index c12c15b91..3d81afe60 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -57,14 +57,15 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } - _handleAuthorizationExpired (error, address, conn, database) { + _handleAuthorizationExpired (error, address, connection, database) { this._log.warn( `Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) + + this._authenticationProvider.handleError({ connection, code: error.code }) + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { this._connectionPool.apply(address, (conn) => conn.authToken === null) - } else { - this._refreshToken() } return error diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index eea52e2b2..075eb6675 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -20,6 +20,7 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection' import Pool, { PoolConfig } from '../pool' import { error, ConnectionProvider, ServerInfo } from 'neo4j-driver-core' +import AuthenticationProvider from './authorization-provider' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { @@ -32,9 +33,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log - this._userAgent = userAgent - this._renewableAuthToken = undefined - this._authTokenProvider = authTokenProvider + this._authenticationProvider = new AuthenticationProvider({ authTokenProvider, userAgent }) this._createChannelConnection = createChannelConnectionHook || (address => { @@ -76,71 +75,14 @@ export default class PooledConnectionProvider extends ConnectionProvider { return release(address, connection) } this._openConnections[connection.id] = connection - return this._getAuthToken() - .then(authToken => connection - .connect(this._userAgent, authToken) - .catch(error => { - // let's destroy this connection - this._destroyConnection(connection) - // propagate the error because connection failed to connect / initialize - throw error - })) - }) - } - - async _getAuthToken () { - if (!this._isRenewableTokenAcquired() || this._isRenewableTokenExpired()) { - this._refreshToken() - } - - if (this._refreshObserver) { - const promiseState = {} - const promise = new Promise((resolve, reject) => { - promiseState.resolve = resolve - promiseState.reject = reject - }) - - this._refreshObserver.subscribe({ - onSuccess: promiseState.resolve, - onError: promiseState.onError - }) - - await promise - } - - return this._renewableAuthToken.authToken - } - - _refreshToken () { - if (!this._refreshObserver) { - const subscribers = [] - this._refreshObserver = { - subscribe: (sub) => subscribers.push(sub), - notify: () => subscribers.forEach(sub => sub.onSuccess()), - notifyError: (e) => subscribers.forEach(sub => sub.onError(e)) - } - Promise.resolve(this._authTokenProvider()) - .then(token => { - this._renewableAuthToken = token - this._refreshObserver.notify() - return token - }) - .catch(e => { - this._refreshObserver.notifyError(e) + return this._authenticationProvider.authenticate({ connection }) + .catch(error => { + // let's destroy this connection + this._destroyConnection(connection) + // propagate the error because connection failed to connect / initialize + throw error }) - .finally(() => { - this._refreshObserver = undefined - }) - } - } - - _isRenewableTokenAcquired () { - return !!this._renewableAuthToken - } - - _isRenewableTokenExpired () { - return this._renewableAuthToken.expectedExpirationTime && - this._renewableAuthToken.expectedExpirationTime < new Date() + }) } /** @@ -159,16 +101,13 @@ export default class PooledConnectionProvider extends ConnectionProvider { return false } - if (this._renewableAuthToken.authToken !== conn.authToken || this._isRenewableTokenExpired()) { - if (!conn.supportsReAuth) { - return false - } - try { - const authToken = await this._getAuthToken() - await conn.reAuth(authToken) - } catch (e) { - return false - } + try { + await this._authenticationProvider.authenticate({ connection: conn }) + } catch (error) { + this._log.info( + `The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'` + ) + return false } return true diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index c26aaf96e..59dc121bb 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -109,17 +109,18 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return error } - _handleAuthorizationExpired (error, address, conn, database) { + _handleAuthorizationExpired (error, address, connection, database) { this._log.warn( `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) + + this._authenticationProvider.handleError({ connection, code: error.code }) if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { this._connectionPool.apply(address, (conn) => conn.authToken === null) - } else { - this._refreshToken() } + return error } diff --git a/packages/core/src/connection.ts b/packages/core/src/connection.ts index eba5b2e98..80749db12 100644 --- a/packages/core/src/connection.ts +++ b/packages/core/src/connection.ts @@ -37,6 +37,9 @@ class Connection { return {} } + /** + * @property {object} authToken The auth registered in the connection + */ get authToken (): any { return {} } @@ -55,6 +58,9 @@ class Connection { return undefined } + /** + * @property {boolean} supportsReAuth Indicates the connection supports re-auth + */ get supportsReAuth (): boolean { return false } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js new file mode 100644 index 000000000..1b33f9161 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js @@ -0,0 +1,116 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Class which provides Authorization for {@link Connection} + */ +export default class AuthenticationProvider { + constructor ({ authTokenProvider, userAgent }) { + this._getAuthToken = authTokenProvider + this._renewableAuthToken = undefined + this._userAgent = userAgent + this._refreshObserver = undefined + } + + async authenticate ({ connection }) { + if (!this._authToken) { + await this._getFreshAuthToken() + } + + if (this._renewableAuthToken.authToken !== connection.authToken || this._isTokenExpired()) { + return await connection.connect(this._userAgent, this._authToken) + } + + return connection + } + + async handleError ({ connection, code }) { + if ( + connection.authToken === this._authToken && + [ + 'Neo.ClientError.Security.Unauthorized', + 'Neo.ClientError.Security.TokenExpired' + ].includes(code) + ) { + this._scheduleRefresh() + } + } + + get _authToken () { + if (this._renewableAuthToken) { + return this._renewableAuthToken.authToken + } + return undefined + } + + get _isTokenExpired () { + return !this._renewableAuthToken || + (this._renewableAuthToken.expectedExpirationTime && + this._renewableAuthToken.expectedExpirationTime < new Date()) + } + + async _getFreshAuthToken () { + if (this._isTokenExpired) { + const promiseState = {} + const promise = new Promise((resolve, reject) => { + promiseState.resolve = resolve + promiseState.reject = reject + }) + + this.scheduleRefresh({ + onSuccess: promiseState.resolve, + onError: promiseState.onError + }) + + await promise + } + + return this._authToken + } + + _scheduleRefresh (observer) { + // there is no refresh schedule + if (!this._refreshObserver) { + const subscribers = [] + + this._refreshObserver = { + subscribe: (sub) => subscribers.push(sub), + notify: () => subscribers.forEach(sub => sub.onSuccess()), + notifyError: (e) => subscribers.forEach(sub => sub.onError(e)) + } + + Promise.resolve(this._getAuthToken()) + .then(token => { + this._renewableAuthToken = token + this._refreshObserver.notify() + return token + }) + .catch(e => { + this._refreshObserver.notifyError(e) + }) + .finally(() => { + this._refreshObserver = undefined + }) + } + + if (observer) { + this._refreshObserver.subscribe(observer) + } + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 9895701c7..33a3720ea 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -32,8 +32,8 @@ const { const { SERVICE_UNAVAILABLE } = error export default class DirectConnectionProvider extends PooledConnectionProvider { - constructor ({ id, config, log, address, userAgent, authToken }) { - super({ id, config, log, userAgent, authToken }) + constructor ({ id, config, log, address, userAgent, authTokenProvider }) { + super({ id, config, log, userAgent, authTokenProvider }) this._address = address } @@ -45,8 +45,8 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { acquireConnection ({ accessMode, database, bookmarks } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, - handleAuthorizationExpired: (error, address) => - this._handleAuthorizationExpired(error, address, database) + handleAuthorizationExpired: (error, address, conn) => + this._handleAuthorizationExpired(error, address, conn, database) }) return this._connectionPool @@ -57,11 +57,17 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } - _handleAuthorizationExpired (error, address, database) { + _handleAuthorizationExpired (error, address, connection, database) { this._log.warn( `Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - this._connectionPool.purge(address).catch(() => {}) + + this._authenticationProvider.handleError({ connection, code: error.code }) + + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { + this._connectionPool.apply(address, (conn) => conn.authToken === null) + } + return error } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 03651b15b..59d585307 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -20,11 +20,12 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection/index.js' import Pool, { PoolConfig } from '../pool/index.js' import { error, ConnectionProvider, ServerInfo } from '../../core/index.ts' +import AuthenticationProvider from './authorization-provider.js' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { constructor ( - { id, config, log, userAgent, authToken }, + { id, config, log, userAgent, authTokenProvider }, createChannelConnectionHook = null ) { super() @@ -32,8 +33,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log - this._userAgent = userAgent - this._authToken = authToken + this._authenticationProvider = new AuthenticationProvider({ authTokenProvider, userAgent }) this._createChannelConnection = createChannelConnectionHook || (address => { @@ -75,8 +75,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { return release(address, connection) } this._openConnections[connection.id] = connection - return connection - .connect(this._userAgent, this._authToken) + return this._authenticationProvider.authenticate({ connection }) .catch(error => { // let's destroy this connection this._destroyConnection(connection) @@ -91,14 +90,27 @@ export default class PooledConnectionProvider extends ConnectionProvider { * @return {boolean} true if the connection is open * @access private **/ - _validateConnection (conn) { + async _validateConnection (conn) { if (!conn.isOpen()) { return false } const maxConnectionLifetime = this._config.maxConnectionLifetime const lifetime = Date.now() - conn.creationTimestamp - return lifetime <= maxConnectionLifetime + if (lifetime > maxConnectionLifetime) { + return false + } + + try { + await this._authenticationProvider.authenticate({ connection: conn }) + } catch (error) { + this._log.info( + `The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'` + ) + return false + } + + return true } /** diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 95480286c..e8465eedf 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -65,10 +65,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider config, log, userAgent, - authToken, + authTokenProvider, routingTablePurgeDelay }) { - super({ id, config, log, userAgent, authToken }, address => { + super({ id, config, log, userAgent, authTokenProvider }, address => { return createChannelConnection( address, this._config, @@ -109,11 +109,18 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return error } - _handleAuthorizationExpired (error, address, database) { + _handleAuthorizationExpired (error, address, connection, database) { this._log.warn( `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - this._connectionPool.purge(address).catch(() => {}) + + this._authenticationProvider.handleError({ connection, code: error.code }) + + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { + this._connectionPool.apply(address, (conn) => conn.authToken === null) + } + + return error } @@ -133,7 +140,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { let name let address const context = { database: database || DEFAULT_DB_NAME } @@ -142,8 +149,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider SESSION_EXPIRED, (error, address) => this._handleUnavailability(error, address, context.database), (error, address) => this._handleWriteFailure(error, address, context.database), - (error, address) => - this._handleAuthorizationExpired(error, address, context.database) + (error, address, conn) => + this._handleAuthorizationExpired(error, address, conn, context.database) ) const routingTable = await this._freshRoutingTable({ @@ -151,6 +158,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider database: context.database, bookmarks, impersonatedUser, + auth, onDatabaseNameResolved: (databaseName) => { context.database = context.database || databaseName if (onDatabaseNameResolved) { @@ -185,6 +193,17 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider routingTable ) + if (auth && auth !== connection.authToken) { + console.log('need to re-auth') + if (connection.supportsReAuth) { + connection.reAuth(auth, true) + } else { + await connection._release() + console.log('create sticky connection') + return await this._createStickyConnection({ address, auth }) + } + } + return new DelegateConnection(connection, databaseSpecificErrorHandler) } catch (error) { const transformed = databaseSpecificErrorHandler.handleAndTransformError( @@ -304,7 +323,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return this._connectionPool.acquire(address) } - _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved } = {}) { + _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { const currentRoutingTable = this._routingTableRegistry.get( database, () => new RoutingTable({ database }) @@ -316,10 +335,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._log.info( `Routing table is stale for database: "${database}" and access mode: "${accessMode}": ${currentRoutingTable}` ) - return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved) + return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) } - _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved) { + _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) { const knownRouters = currentRoutingTable.routers if (this._useSeedRouter) { @@ -328,7 +347,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - onDatabaseNameResolved + onDatabaseNameResolved, + auth ) } return this._fetchRoutingTableFromKnownRoutersFallbackToSeedRouter( @@ -336,7 +356,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - onDatabaseNameResolved + onDatabaseNameResolved, + auth ) } @@ -345,7 +366,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - onDatabaseNameResolved + onDatabaseNameResolved, + auth ) { // we start with seed router, no routers were probed before const seenRouters = [] @@ -354,7 +376,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._seedRouter, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) if (newRoutingTable) { @@ -365,7 +388,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider knownRouters, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) newRoutingTable = newRoutingTable2 error = error2 || error @@ -384,13 +408,15 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - onDatabaseNameResolved + onDatabaseNameResolved, + auth ) { let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters( knownRouters, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) if (!newRoutingTable) { @@ -400,7 +426,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._seedRouter, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) } @@ -416,13 +443,15 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider knownRouters, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) { const [newRoutingTable, error] = await this._fetchRoutingTable( knownRouters, currentRoutingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) if (newRoutingTable) { @@ -447,7 +476,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider seedRouter, routingTable, bookmarks, - impersonatedUser + impersonatedUser, + auth ) { const resolvedAddresses = await this._resolveSeedRouter(seedRouter) @@ -456,7 +486,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider address => seenRouters.indexOf(address) < 0 ) - return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser) + return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth) } async _resolveSeedRouter (seedRouter) { @@ -468,7 +498,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return [].concat.apply([], dnsResolvedAddresses) } - async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser) { + async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth) { return routerAddresses.reduce( async (refreshedTablePromise, currentRouter, currentIndex) => { const [newRoutingTable] = await refreshedTablePromise @@ -491,7 +521,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const [session, error] = await this._createSessionForRediscovery( currentRouter, bookmarks, - impersonatedUser + impersonatedUser, + auth ) if (session) { try { @@ -516,13 +547,38 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser) { + async _createStickyConnection ({ address, auth }) { + const connection = await this._createChannelConnection(address) + connection._release = () => this._destroyConnection(connection) + this._openConnections[connection.id] = connection + try { - const connection = await this._connectionPool.acquire(routerAddress) + return await connection.connect(this._userAgent, auth) + } catch (error) { + await this._destroyConnection() + throw error + } + } + + async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth) { + try { + let connection = await this._connectionPool.acquire(routerAddress) + + if (auth && connection.authToken !== auth) { + if (connection.supportsReAuth) { + await connection.reAuth(auth) + } else { + await connection._release() + connection = await this._createStickyConnection({ + address: routerAddress, + auth + }) + } + } const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SESSION_EXPIRED, - handleAuthorizationExpired: (error, address) => this._handleAuthorizationExpired(error, address) + handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn) }) const connectionProvider = new SingleConnectionProvider( @@ -714,6 +770,7 @@ function _isFailFastError (error) { } function _isFailFastSecurityError (error) { + console.error(error) return error.code.startsWith('Neo.ClientError.Security.') && ![ AUTHORIZATION_EXPIRED_CODE diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/home-database-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/home-database-provider.js new file mode 100644 index 000000000..9f28d1fbf --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/home-database-provider.js @@ -0,0 +1,32 @@ + + + +export class HomeDatabaseProvider { + constructor(ttlInSeconds = -1, cache = new Map()) { + this._ttlMs = ttlInSeconds * 1000 + this._cache = cache + } + + getDatabaseName ({ database, auth, impersonatedUser }) { + return database + if (database != null && database !== '') { + return database + } + + const key = impersonatedUser || auth || null + + if (this._cache.has(key)) { + const { createdAt, database: resolvedDatabase } = this._cache.get(key) + if ((Date.now() - createdAt) < this._ttlMs) { + return resolvedDatabase + } + } + + return database + } + + setDatabaseName ({ database, auth, impersonatedUser }) { + const key = impersonatedUser || auth || null + this._cache.set(key, { createdAt: Date.now(), database }) + } +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 79dd0e4b9..7c733ba78 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -124,7 +124,7 @@ export default class ChannelConnection extends Connection { protocolSupplier ) { super(errorHandler) - + this._authToken = null this._reseting = false this._resetObservers = [] this._id = idGenerator++ @@ -148,6 +148,8 @@ export default class ChannelConnection extends Connection { */ this._protocol = protocolSupplier(this) + this._supportsReAuth = this._protocol.version > 5.0 + // Set to true on fatal errors, to get this out of connection pool. this._isBroken = false @@ -156,6 +158,18 @@ export default class ChannelConnection extends Connection { } } + get authToken () { + return this._authToken + } + + set authToken (value) { + this._authToken = value + } + + get supportsReAuth () { + return this._supportsReAuth + } + get id () { return this._id } @@ -175,9 +189,16 @@ export default class ChannelConnection extends Connection { * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ connect (userAgent, authToken) { + this._authToken = authToken return this._initialize(userAgent, authToken) } + async reAuth (authToken) { + this._authToken = authToken + console.log('re aut') + return this + } + /** * Perform protocol-specific initialization which includes authentication. * @param {string} userAgent the user agent for this driver. diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js index 6d195d1d9..973c2a970 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js @@ -51,6 +51,18 @@ export default class DelegateConnection extends Connection { return this._delegate.server } + get authToken () { + return this._delegate.authToken + } + + get supportsReAuth () { + return this._delegate.supportsReAuth + } + + set authToken (value) { + this._delegate.authToken = value + } + get address () { return this._delegate.address } @@ -67,6 +79,10 @@ export default class DelegateConnection extends Connection { return this._delegate.isOpen() } + reAuth (authToken) { + return this._delegate.reAuth(authToken) + } + protocol () { return this._delegate.protocol() } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js index 8544c4c10..ebe305e26 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-error-handler.js @@ -62,15 +62,15 @@ export default class ConnectionErrorHandler { * @param {ServerAddress} address the address of the connection where the error happened. * @return {Neo4jError} new error that should be propagated to the user. */ - handleAndTransformError (error, address) { + handleAndTransformError (error, address, connection) { if (isAutorizationExpiredError(error)) { - return this._handleAuthorizationExpired(error, address) + return this._handleAuthorizationExpired(error, address, connection) } if (isAvailabilityError(error)) { - return this._handleUnavailability(error, address) + return this._handleUnavailability(error, address, connection) } if (isFailureToWrite(error)) { - return this._handleWriteFailure(error, address) + return this._handleWriteFailure(error, address, connection) } return error } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js index dc996522c..d9856921d 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection.js @@ -39,6 +39,18 @@ export default class Connection { throw new Error('not implemented') } + get authToken () { + throw new Error('not implemented') + } + + set authToken (value) { + throw new Error('not implemented') + } + + get supportsReAuth () { + throw new Error('not implemented') + } + /** * @returns {boolean} whether this connection is in a working condition */ @@ -124,7 +136,7 @@ export default class Connection { */ handleAndTransformError (error, address) { if (this._errorHandler) { - return this._errorHandler.handleAndTransformError(error, address) + return this._errorHandler.handleAndTransformError(error, address, this) } return error diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js index 148c5f1b9..ec14ce0ee 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js @@ -123,6 +123,14 @@ class Pool { return this._purgeKey(address.asKey()) } + apply (address, resourceConsumer) { + const key = address.asKey() + + if (key in this._pools) { + this._pools[key].apply(resourceConsumer) + } + } + /** * Destroy all idle resources in this pool. * @returns {Promise} A promise that is resolved when the resources are purged @@ -195,7 +203,7 @@ class Pool { while (pool.length) { const resource = pool.pop() - if (this._validate(resource)) { + if (await this._validate(resource)) { if (this._removeIdleObserver) { this._removeIdleObserver(resource) } @@ -207,6 +215,7 @@ class Pool { } return { resource, pool } } else { + pool.removeInUse(resource) await this._destroy(resource) } } @@ -231,6 +240,7 @@ class Pool { // Invoke callback that creates actual connection resource = await this._create(address, (address, resource) => this._release(address, resource, pool)) + pool.pushInUse(resource) resourceAcquired(key, this._activeResourceCounts) if (this._log.isDebugEnabled()) { this._log.debug(`${resource} created for the pool ${key}`) @@ -246,12 +256,13 @@ class Pool { if (pool.isActive()) { // there exist idle connections for the given key - if (!this._validate(resource)) { + if (!await this._validate(resource)) { if (this._log.isDebugEnabled()) { this._log.debug( `${resource} destroyed and can't be released to the pool ${key} because it is not functional` ) } + pool.removeInUse(resource) await this._destroy(resource) } else { if (this._installIdleObserver) { @@ -263,6 +274,7 @@ class Pool { const pool = this._pools[key] if (pool) { this._pools[key] = pool.filter(r => r !== resource) + pool.removeInUse(resource) } // let's not care about background clean-ups due to errors but just trigger the destroy // process for the resource, we especially catch any errors and ignore them to avoid @@ -283,6 +295,7 @@ class Pool { `${resource} destroyed and can't be released to the pool ${key} because pool has been purged` ) } + pool.removeInUse(resource) await this._destroy(resource) } resourceReleased(key, this._activeResourceCounts) @@ -419,6 +432,7 @@ class SingleAddressPool { constructor () { this._active = true this._elements = [] + this._elementsInUse = new Set() } isActive () { @@ -427,6 +441,8 @@ class SingleAddressPool { close () { this._active = false + this._elements = [] + this._elementsInUse = new Set() } filter (predicate) { @@ -434,17 +450,33 @@ class SingleAddressPool { return this } + apply (resourceConsumer) { + this._elements.forEach(resourceConsumer) + this._elementsInUse.forEach(resourceConsumer) + } + get length () { return this._elements.length } pop () { - return this._elements.pop() + const element = this._elements.pop() + this._elementsInUse.add(element) + return element } push (element) { + this._elementsInUse.delete(element) return this._elements.push(element) } + + pushInUse (element) { + this._elementsInUse.add(element) + } + + removeInUse (element) { + this._elementsInUse.delete(element) + } } export default Pool diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts index 0326d5931..e0ed37df9 100644 --- a/packages/neo4j-driver-deno/lib/core/connection-provider.ts +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -21,6 +21,7 @@ import Connection from './connection.ts' import { bookmarks } from './internal/index.ts' import { ServerInfo } from './result-summary.ts' +import { AuthToken } from './types.ts' /** * Inteface define a common way to acquire a connection @@ -51,6 +52,7 @@ class ConnectionProvider { bookmarks: bookmarks.Bookmarks impersonatedUser?: string onDatabaseNameResolved?: (databaseName?: string) => void + auth?: AuthToken }): Promise { throw Error('Not implemented') } diff --git a/packages/neo4j-driver-deno/lib/core/connection.ts b/packages/neo4j-driver-deno/lib/core/connection.ts index 30c896932..c97c873bb 100644 --- a/packages/neo4j-driver-deno/lib/core/connection.ts +++ b/packages/neo4j-driver-deno/lib/core/connection.ts @@ -37,6 +37,13 @@ class Connection { return {} } + /** + * @property {object} authToken The auth registered in the connection + */ + get authToken (): any { + return {} + } + /** * @property {ServerAddress} the server address this connection is opened against */ @@ -51,6 +58,13 @@ class Connection { return undefined } + /** + * @property {boolean} supportsReAuth Indicates the connection supports re-auth + */ + get supportsReAuth (): boolean { + return false + } + /** * @returns {boolean} whether this connection is in a working condition */ diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index aa61ee18e..0b6cf4318 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -38,7 +38,8 @@ import { LoggingConfig, TrustStrategy, SessionMode, - Query + Query, + AuthToken } from './types.ts' import { ServerAddress } from './internal/server-address.ts' import BookmarkManager, { bookmarkManager } from './bookmark-manager.ts' @@ -96,6 +97,7 @@ type CreateSession = (args: { impersonatedUser?: string bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter + auth?: AuthToken }) => Session type CreateQueryExecutor = (createSession: (config: { database?: string, bookmarkManager?: BookmarkManager }) => Session) => QueryExecutor @@ -121,6 +123,7 @@ class SessionConfig { fetchSize?: number bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter + auth?: AuthToken /** * @constructor @@ -696,7 +699,8 @@ class Driver { impersonatedUser, fetchSize, bookmarkManager, - notificationFilter + notificationFilter, + auth }: SessionConfig = {}): Session { return this._newSession({ defaultAccessMode, @@ -707,7 +711,8 @@ class Driver { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize!), bookmarkManager, - notificationFilter + notificationFilter, + auth }) } @@ -746,7 +751,8 @@ class Driver { impersonatedUser, fetchSize, bookmarkManager, - notificationFilter + notificationFilter, + auth }: { defaultAccessMode: SessionMode bookmarkOrBookmarks?: string | string[] @@ -755,7 +761,8 @@ class Driver { impersonatedUser?: string fetchSize: number bookmarkManager?: BookmarkManager - notificationFilter?: NotificationFilter + notificationFilter?: NotificationFilter, + auth?: AuthToken }): Session { const sessionMode = Session._validateSessionMode(defaultAccessMode) const connectionProvider = this._getOrCreateConnectionProvider() @@ -773,7 +780,8 @@ class Driver { impersonatedUser, fetchSize, bookmarkManager, - notificationFilter + notificationFilter, + auth }) } diff --git a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts index d67044100..6cc6c7e5b 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts @@ -24,6 +24,7 @@ import Connection from '../connection.ts' import { ACCESS_MODE_WRITE } from './constants.ts' import { Bookmarks } from './bookmarks.ts' import ConnectionProvider from '../connection-provider.ts' +import { AuthToken } from '../types.ts' /** * @private @@ -85,6 +86,8 @@ class ConnectionHolder implements ConnectionHolderInterface { private readonly _impersonatedUser?: string private readonly _getConnectionAcquistionBookmarks: () => Promise private readonly _onDatabaseNameResolved?: (databaseName?: string) => void + private readonly _auth?: AuthToken + private _closed: boolean /** * @constructor @@ -104,7 +107,8 @@ class ConnectionHolder implements ConnectionHolderInterface { connectionProvider, impersonatedUser, onDatabaseNameResolved, - getConnectionAcquistionBookmarks + getConnectionAcquistionBookmarks, + auth }: { mode?: string database?: string @@ -113,8 +117,10 @@ class ConnectionHolder implements ConnectionHolderInterface { impersonatedUser?: string onDatabaseNameResolved?: (databaseName?: string) => void getConnectionAcquistionBookmarks?: () => Promise + auth?: AuthToken } = {}) { this._mode = mode + this._closed = false this._database = database != null ? assertString(database, 'database') : '' this._bookmarks = bookmarks ?? Bookmarks.empty() this._connectionProvider = connectionProvider @@ -122,6 +128,7 @@ class ConnectionHolder implements ConnectionHolderInterface { this._referenceCount = 0 this._connectionPromise = Promise.resolve(null) this._onDatabaseNameResolved = onDatabaseNameResolved + this._auth = auth this._getConnectionAcquistionBookmarks = getConnectionAcquistionBookmarks ?? (() => Promise.resolve(Bookmarks.empty())) } @@ -166,7 +173,8 @@ class ConnectionHolder implements ConnectionHolderInterface { database: this._database, bookmarks: await this._getBookmarks(), impersonatedUser: this._impersonatedUser, - onDatabaseNameResolved: this._onDatabaseNameResolved + onDatabaseNameResolved: this._onDatabaseNameResolved, + auth: this._auth }) } @@ -192,6 +200,7 @@ class ConnectionHolder implements ConnectionHolderInterface { } close (hasTx?: boolean): Promise { + this._closed = true if (this._referenceCount === 0) { return this._connectionPromise } @@ -216,6 +225,10 @@ class ConnectionHolder implements ConnectionHolderInterface { .catch(ignoreError) .then(() => connection._release().then(() => null)) } + + if (!this._closed && (this._auth != null) && !connection.supportsReAuth) { + return connection + } return connection._release().then(() => null) } else { return Promise.resolve(null) diff --git a/packages/neo4j-driver-deno/lib/core/session.ts b/packages/neo4j-driver-deno/lib/core/session.ts index 26fea9162..246d7e0dd 100644 --- a/packages/neo4j-driver-deno/lib/core/session.ts +++ b/packages/neo4j-driver-deno/lib/core/session.ts @@ -30,7 +30,7 @@ import { TransactionExecutor } from './internal/transaction-executor.ts' import { Bookmarks } from './internal/bookmarks.ts' import { TxConfig } from './internal/tx-config.ts' import ConnectionProvider from './connection-provider.ts' -import { Query, SessionMode } from './types.ts' +import { AuthToken, Query, SessionMode } from './types.ts' import Connection from './connection.ts' import { NumberOrInteger } from './graph-types.ts' import TransactionPromise from './transaction-promise.ts' @@ -98,7 +98,8 @@ class Session { fetchSize, impersonatedUser, bookmarkManager, - notificationFilter + notificationFilter, + auth }: { mode: SessionMode connectionProvider: ConnectionProvider @@ -110,6 +111,7 @@ class Session { impersonatedUser?: string bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter + auth?: AuthToken }) { this._mode = mode this._database = database @@ -119,6 +121,7 @@ class Session { this._getConnectionAcquistionBookmarks = this._getConnectionAcquistionBookmarks.bind(this) this._readConnectionHolder = new ConnectionHolder({ mode: ACCESS_MODE_READ, + auth, database, bookmarks, connectionProvider, @@ -128,6 +131,7 @@ class Session { }) this._writeConnectionHolder = new ConnectionHolder({ mode: ACCESS_MODE_WRITE, + auth, database, bookmarks, connectionProvider, diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index 63140cc16..ccd93ead2 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -48,6 +48,15 @@ export interface AuthToken { realm?: string parameters?: Parameters } + +export interface RenewableAuthToken { + expectedExpirationTime?: Date + authToken: AuthToken +} + +// Can be async, the user probably wants to do some IO. +export type AuthTokenProvider = () => Promise | RenewableAuthToken + export interface Config { encrypted?: boolean | EncryptionLevel trust?: TrustStrategy diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 633060a56..8a32f8491 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -102,6 +102,8 @@ import { } from './bolt-connection/index.js' type AuthToken = coreTypes.AuthToken +type RenewableAuthToken = coreTypes.RenewableAuthToken +type AuthTokenProvider = coreTypes.AuthTokenProvider type Config = coreTypes.Config type TrustStrategy = coreTypes.TrustStrategy type EncryptionLevel = coreTypes.EncryptionLevel @@ -117,6 +119,22 @@ const { urlUtil } = internal +function createAuthProvider (authTokenOrProvider: AuthToken | AuthTokenProvider): AuthTokenProvider { + if (typeof authTokenOrProvider === 'function') { + return authTokenOrProvider + } + + let authToken: AuthToken = authTokenOrProvider + // Sanitize authority token. Nicer error from server when a scheme is set. + authToken = authToken ?? {} + authToken.scheme = authToken.scheme ?? 'none' + return function (): RenewableAuthToken { + return { + authToken + } + } +} + /** * Construct a new Neo4j Driver. This is your main entry point for this * library. @@ -247,13 +265,13 @@ const { * } * * @param {string} url The URL for the Neo4j database, for instance "neo4j://localhost" and/or "bolt://localhost" - * @param {Map} authToken Authentication credentials. See {@link auth} for helpers. + * @param {Map| function()} authToken Authentication credentials. See {@link auth} for helpers. * @param {Object} config Configuration object. See the configuration section above for details. * @returns {Driver} */ function driver ( url: string, - authToken: AuthToken, + authToken: AuthToken | AuthTokenProvider, config: Config = {} ): Driver { assertString(url, 'Bolt URL') @@ -303,9 +321,7 @@ function driver ( config.trust = trust } - // Sanitize authority token. Nicer error from server when a scheme is set. - authToken = authToken ?? {} - authToken.scheme = authToken.scheme ?? 'none' + const authTokenProvider = createAuthProvider(authToken) // Use default user agent or user agent specified by user. config.userAgent = config.userAgent ?? USER_AGENT @@ -332,7 +348,7 @@ function driver ( config, log, hostNameResolver, - authToken, + authTokenProvider, address, userAgent: config.userAgent, routingContext: parsedUrl.query @@ -349,7 +365,7 @@ function driver ( id, config, log, - authToken, + authTokenProvider, address, userAgent: config.userAgent }) @@ -586,6 +602,8 @@ export { export type { QueryResult, AuthToken, + AuthTokenProvider, + RenewableAuthToken, Config, EncryptionLevel, TrustStrategy, From a7f441adf1863663f5505d5acbc878cb1a51a26e Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 9 Jan 2023 17:00:32 +0100 Subject: [PATCH 06/70] Fix tests --- .../authorization-provider.js | 6 ++-- .../src/connection/connection-channel.js | 2 +- .../connection-provider-direct.test.js | 12 +++++--- .../connection-provider-routing.test.js | 30 +++++++++++-------- .../authorization-provider.js | 2 +- .../connection/connection-channel.js | 2 +- .../test/internal/fake-connection.js | 4 +++ 7 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/authorization-provider.js b/packages/bolt-connection/src/connection-provider/authorization-provider.js index 1b33f9161..0637f9258 100644 --- a/packages/bolt-connection/src/connection-provider/authorization-provider.js +++ b/packages/bolt-connection/src/connection-provider/authorization-provider.js @@ -22,7 +22,7 @@ */ export default class AuthenticationProvider { constructor ({ authTokenProvider, userAgent }) { - this._getAuthToken = authTokenProvider + this._getAuthToken = authTokenProvider || (() => ({})) this._renewableAuthToken = undefined this._userAgent = userAgent this._refreshObserver = undefined @@ -41,8 +41,8 @@ export default class AuthenticationProvider { } async handleError ({ connection, code }) { - if ( - connection.authToken === this._authToken && + if ( + connection.authToken === this._authToken && [ 'Neo.ClientError.Security.Unauthorized', 'Neo.ClientError.Security.TokenExpired' diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index ed8413c77..85bbbe9e6 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -148,7 +148,7 @@ export default class ChannelConnection extends Connection { */ this._protocol = protocolSupplier(this) - this._supportsReAuth = this._protocol.version > 5.0 + this._supportsReAuth = this._protocol ? this._protocol.version > 5.0 : false // TODO: Move logic to the protocol // Set to true on fatal errors, to get this out of connection pool. this._isBroken = false diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 2b659de63..ef717f762 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -56,7 +56,7 @@ describe('#unit DirectConnectionProvider', () => { expect(conn instanceof DelegateConnection).toBeTruthy() }) - it('should purge connections for address when AuthorizationExpired happens', async () => { + it('should not purge connections for address when AuthorizationExpired happens', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() jest.spyOn(pool, 'purge') @@ -74,7 +74,7 @@ describe('#unit DirectConnectionProvider', () => { conn.handleAndTransformError(error, address) - expect(pool.purge).toHaveBeenCalledWith(address) + expect(pool.purge).not.toHaveBeenCalledWith(address) }) it('should purge not change error when AuthorizationExpired happens', async () => { @@ -98,7 +98,7 @@ describe('#unit DirectConnectionProvider', () => { }) }) -it('should purge connections for address when TokenExpired happens', async () => { +it('should not purge connections for address when TokenExpired happens', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() jest.spyOn(pool, 'purge') @@ -116,7 +116,7 @@ it('should purge connections for address when TokenExpired happens', async () => conn.handleAndTransformError(error, address) - expect(pool.purge).toHaveBeenCalledWith(address) + expect(pool.purge).not.toHaveBeenCalledWith(address) }) it('should not change error when TokenExpired happens', async () => { @@ -347,6 +347,10 @@ class FakeConnection extends Connection { this._server = server } + get authToken () { + return this._authToken + } + get address () { return this._address } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 8e742cf3c..91994f78b 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -1434,7 +1434,7 @@ describe.each([ }) }, 10000) - it.each(usersDataSet)('should purge connections for address when AuthorizationExpired happens [user=%s]', async (user) => { + it.each(usersDataSet)('should not purge connections for address when AuthorizationExpired happens [user=%s]', async (user) => { const pool = newPool() jest.spyOn(pool, 'purge') @@ -1471,8 +1471,8 @@ describe.each([ server3Connection.handleAndTransformError(error, server3) server2Connection.handleAndTransformError(error, server2) - expect(pool.purge).toHaveBeenCalledWith(server3) - expect(pool.purge).toHaveBeenCalledWith(server2) + expect(pool.purge).not.toHaveBeenCalledWith(server3) + expect(pool.purge).not.toHaveBeenCalledWith(server2) }) it.each(usersDataSet)('should purge not change error when AuthorizationExpired happens [user=%s]', async (user) => { @@ -1511,7 +1511,7 @@ describe.each([ expect(error).toBe(expectedError) }) - it.each(usersDataSet)('should purge connections for address when TokenExpired happens [user=%s]', async (user) => { + it.each(usersDataSet)('should not purge connections for address when TokenExpired happens [user=%s]', async (user) => { const pool = newPool() jest.spyOn(pool, 'purge') @@ -1548,8 +1548,8 @@ describe.each([ server3Connection.handleAndTransformError(error, server3) server2Connection.handleAndTransformError(error, server2) - expect(pool.purge).toHaveBeenCalledWith(server3) - expect(pool.purge).toHaveBeenCalledWith(server2) + expect(pool.purge).not.toHaveBeenCalledWith(server3) + expect(pool.purge).not.toHaveBeenCalledWith(server2) }) it.each(usersDataSet)('should not change error when TokenExpired happens [user=%s]', async (user) => { @@ -1784,7 +1784,7 @@ describe.each([ expect(conn2.address).toBe(serverA) }, 10000) - it.each(usersDataSet)('should purge connections for address when AuthorizationExpired happens [user=%s]', async (user) => { + it.each(usersDataSet)('should not purge connections for address when AuthorizationExpired happens [user=%s]', async (user) => { const pool = newPool() jest.spyOn(pool, 'purge') @@ -1822,11 +1822,11 @@ describe.each([ serverAConnection.handleAndTransformError(error, serverA) server2Connection.handleAndTransformError(error, server2) - expect(pool.purge).toHaveBeenCalledWith(serverA) - expect(pool.purge).toHaveBeenCalledWith(server2) + expect(pool.purge).not.toHaveBeenCalledWith(serverA) + expect(pool.purge).not.toHaveBeenCalledWith(server2) }) - it.each(usersDataSet)('should purge not change error when AuthorizationExpired happens [user=%s]', async (user) => { + it.each(usersDataSet)('should not purge change error when AuthorizationExpired happens [user=%s]', async (user) => { const pool = newPool() const connectionProvider = newRoutingConnectionProvider( @@ -1861,7 +1861,7 @@ describe.each([ expect(error).toBe(expectedError) }) - it.each(usersDataSet)('should purge connections for address when TokenExpired happens [user=%s]', async (user) => { + it.each(usersDataSet)('should not purge connections for address when TokenExpired happens [user=%s]', async (user) => { const pool = newPool() jest.spyOn(pool, 'purge') @@ -1899,8 +1899,8 @@ describe.each([ serverAConnection.handleAndTransformError(error, serverA) server2Connection.handleAndTransformError(error, server2) - expect(pool.purge).toHaveBeenCalledWith(serverA) - expect(pool.purge).toHaveBeenCalledWith(server2) + expect(pool.purge).not.toHaveBeenCalledWith(serverA) + expect(pool.purge).not.toHaveBeenCalledWith(server2) }) it.each(usersDataSet)('should not change error when TokenExpired happens [user=%s]', async (user) => { @@ -3105,6 +3105,10 @@ class FakeConnection extends Connection { this._server = server } + get authToken () { + return this._authToken + } + get address () { return this._address } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js index 1b33f9161..da4459fb1 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js @@ -22,7 +22,7 @@ */ export default class AuthenticationProvider { constructor ({ authTokenProvider, userAgent }) { - this._getAuthToken = authTokenProvider + this._getAuthToken = authTokenProvider || (() => ({})) this._renewableAuthToken = undefined this._userAgent = userAgent this._refreshObserver = undefined diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 7c733ba78..781dbe5bf 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -148,7 +148,7 @@ export default class ChannelConnection extends Connection { */ this._protocol = protocolSupplier(this) - this._supportsReAuth = this._protocol.version > 5.0 + this._supportsReAuth = this._protocol ? this._protocol.version > 5.0 : false // TODO: Move logic to the protocol // Set to true on fatal errors, to get this out of connection pool. this._isBroken = false diff --git a/packages/neo4j-driver/test/internal/fake-connection.js b/packages/neo4j-driver/test/internal/fake-connection.js index fff4d6d4c..415f04f03 100644 --- a/packages/neo4j-driver/test/internal/fake-connection.js +++ b/packages/neo4j-driver/test/internal/fake-connection.js @@ -79,6 +79,10 @@ export default class FakeConnection extends Connection { this._server.version = value } + get authToken () { + return this._authToken + } + protocol () { // return fake protocol object that simply records seen queries and parameters return { From 44c891b20c0aaad05e8f3465f5d5f85824e10e75 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 10 Jan 2023 12:48:47 +0100 Subject: [PATCH 07/70] Adjust test --- .../test/internal/connection-provider-pooled.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/neo4j-driver/test/internal/connection-provider-pooled.test.js b/packages/neo4j-driver/test/internal/connection-provider-pooled.test.js index 159a5a455..d36659237 100644 --- a/packages/neo4j-driver/test/internal/connection-provider-pooled.test.js +++ b/packages/neo4j-driver/test/internal/connection-provider-pooled.test.js @@ -21,20 +21,20 @@ import FakeConnection from './fake-connection' import lolex from 'lolex' describe('#unit PooledConnectionProvider', () => { - it('should treat closed connections as invalid', () => { + it('should treat closed connections as invalid', async () => { const provider = new PooledConnectionProvider({ id: 0, config: {} }) - const connectionValid = provider._validateConnection( + const connectionValid = await provider._validateConnection( new FakeConnection().closed() ) expect(connectionValid).toBeFalsy() }) - it('should treat not old open connections as valid', () => { + xit('should treat not old open connections as valid', async () => { const provider = new PooledConnectionProvider({ id: 0, config: { @@ -46,7 +46,7 @@ describe('#unit PooledConnectionProvider', () => { const clock = lolex.install() try { clock.setSystemTime(20) - const connectionValid = provider._validateConnection(connection) + const connectionValid = await provider._validateConnection(connection) expect(connectionValid).toBeTruthy() } finally { @@ -54,7 +54,7 @@ describe('#unit PooledConnectionProvider', () => { } }) - it('should treat old open connections as invalid', () => { + it('should treat old open connections as invalid', async () => { const provider = new PooledConnectionProvider({ id: 0, config: { @@ -66,7 +66,7 @@ describe('#unit PooledConnectionProvider', () => { const clock = lolex.install() try { clock.setSystemTime(20) - const connectionValid = provider._validateConnection(connection) + const connectionValid = await provider._validateConnection(connection) expect(connectionValid).toBeFalsy() } finally { From b41b729f29a5c038f61695d5e3a561d7cbac8da1 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 10 Jan 2023 14:51:52 +0100 Subject: [PATCH 08/70] Add login/logoff concepts --- .../authentication-provider.js} | 13 +++------ .../connection-provider-pooled.js | 2 +- .../connection-provider-routing.js | 9 ++---- .../src/connection/connection-channel.js | 29 ++++++++++++------- .../src/connection/connection-delegate.js | 4 --- .../authentication-provider.js} | 17 ++++------- .../connection-provider-pooled.js | 2 +- .../connection-provider-routing.js | 9 ++---- .../connection/connection-channel.js | 29 ++++++++++++------- .../connection/connection-delegate.js | 4 --- 10 files changed, 56 insertions(+), 62 deletions(-) rename packages/{neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js => bolt-connection/src/connection-provider/authentication-provider.js} (92%) rename packages/{bolt-connection/src/connection-provider/authorization-provider.js => neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js} (90%) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js similarity index 92% rename from packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js rename to packages/bolt-connection/src/connection-provider/authentication-provider.js index da4459fb1..a76a8a5f1 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authorization-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -67,17 +67,12 @@ export default class AuthenticationProvider { async _getFreshAuthToken () { if (this._isTokenExpired) { - const promiseState = {} const promise = new Promise((resolve, reject) => { - promiseState.resolve = resolve - promiseState.reject = reject - }) - - this.scheduleRefresh({ - onSuccess: promiseState.resolve, - onError: promiseState.onError + this.scheduleRefresh({ + onSuccess: resolve, + onError: reject + }) }) - await promise } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 075eb6675..974e01d63 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -20,7 +20,7 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection' import Pool, { PoolConfig } from '../pool' import { error, ConnectionProvider, ServerInfo } from 'neo4j-driver-core' -import AuthenticationProvider from './authorization-provider' +import AuthenticationProvider from './authentication-provider' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 59dc121bb..7db1c2383 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -113,14 +113,13 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._log.warn( `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - + this._authenticationProvider.handleError({ connection, code: error.code }) if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { this._connectionPool.apply(address, (conn) => conn.authToken === null) } - return error } @@ -194,12 +193,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) if (auth && auth !== connection.authToken) { - console.log('need to re-auth') if (connection.supportsReAuth) { - connection.reAuth(auth, true) + await connection.connect(this._userAgent, auth) } else { await connection._release() - console.log('create sticky connection') return await this._createStickyConnection({ address, auth }) } } @@ -566,7 +563,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider if (auth && connection.authToken !== auth) { if (connection.supportsReAuth) { - await connection.reAuth(auth) + await await connection.connect(this._userAgent, auth) } else { await connection._release() connection = await this._createStickyConnection({ diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 85bbbe9e6..2aa2e7553 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -148,8 +148,6 @@ export default class ChannelConnection extends Connection { */ this._protocol = protocolSupplier(this) - this._supportsReAuth = this._protocol ? this._protocol.version > 5.0 : false // TODO: Move logic to the protocol - // Set to true on fatal errors, to get this out of connection pool. this._isBroken = false @@ -167,7 +165,7 @@ export default class ChannelConnection extends Connection { } get supportsReAuth () { - return this._supportsReAuth + return this._protocol.supportsLogoff } get id () { @@ -188,15 +186,26 @@ export default class ChannelConnection extends Connection { * @param {Object} authToken the object containing auth information. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ - connect (userAgent, authToken) { + async connect (userAgent, authToken) { this._authToken = authToken - return this._initialize(userAgent, authToken) - } + if (!this._protocol.initialized) { + return await this._initialize(userAgent, authToken) + } - async reAuth (authToken) { - this._authToken = authToken - console.log('re aut') - return this + if (!this._protocol.supportsLogoff) { + throw newError('Connection does not support re-auth') + } + + const logoffPromise = new Promise((resolve, reject) => { + this._protocol.logoff({ onComplete: resolve, onError: reject }) + }) + + const loginPromise = new Promise((resolve, reject) => { + this._protocol.login({ onComplete: resolve, onError: reject, flush: true }) + }) + + return await Promise.all(logoffPromise, loginPromise) + .then(() => this) } /** diff --git a/packages/bolt-connection/src/connection/connection-delegate.js b/packages/bolt-connection/src/connection/connection-delegate.js index 925858102..fd8d0aa03 100644 --- a/packages/bolt-connection/src/connection/connection-delegate.js +++ b/packages/bolt-connection/src/connection/connection-delegate.js @@ -79,10 +79,6 @@ export default class DelegateConnection extends Connection { return this._delegate.isOpen() } - reAuth (authToken) { - return this._delegate.reAuth(authToken) - } - protocol () { return this._delegate.protocol() } diff --git a/packages/bolt-connection/src/connection-provider/authorization-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js similarity index 90% rename from packages/bolt-connection/src/connection-provider/authorization-provider.js rename to packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index 0637f9258..a76a8a5f1 100644 --- a/packages/bolt-connection/src/connection-provider/authorization-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -41,8 +41,8 @@ export default class AuthenticationProvider { } async handleError ({ connection, code }) { - if ( - connection.authToken === this._authToken && + if ( + connection.authToken === this._authToken && [ 'Neo.ClientError.Security.Unauthorized', 'Neo.ClientError.Security.TokenExpired' @@ -67,17 +67,12 @@ export default class AuthenticationProvider { async _getFreshAuthToken () { if (this._isTokenExpired) { - const promiseState = {} const promise = new Promise((resolve, reject) => { - promiseState.resolve = resolve - promiseState.reject = reject - }) - - this.scheduleRefresh({ - onSuccess: promiseState.resolve, - onError: promiseState.onError + this.scheduleRefresh({ + onSuccess: resolve, + onError: reject + }) }) - await promise } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 59d585307..9be46c7c8 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -20,7 +20,7 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection/index.js' import Pool, { PoolConfig } from '../pool/index.js' import { error, ConnectionProvider, ServerInfo } from '../../core/index.ts' -import AuthenticationProvider from './authorization-provider.js' +import AuthenticationProvider from './authentication-provider.js' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index e8465eedf..4b9bd58ed 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -113,14 +113,13 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._log.warn( `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - + this._authenticationProvider.handleError({ connection, code: error.code }) if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { this._connectionPool.apply(address, (conn) => conn.authToken === null) } - return error } @@ -194,12 +193,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) if (auth && auth !== connection.authToken) { - console.log('need to re-auth') if (connection.supportsReAuth) { - connection.reAuth(auth, true) + await connection.connect(this._userAgent, auth) } else { await connection._release() - console.log('create sticky connection') return await this._createStickyConnection({ address, auth }) } } @@ -566,7 +563,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider if (auth && connection.authToken !== auth) { if (connection.supportsReAuth) { - await connection.reAuth(auth) + await await connection.connect(this._userAgent, auth) } else { await connection._release() connection = await this._createStickyConnection({ diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 781dbe5bf..bf8357f5a 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -148,8 +148,6 @@ export default class ChannelConnection extends Connection { */ this._protocol = protocolSupplier(this) - this._supportsReAuth = this._protocol ? this._protocol.version > 5.0 : false // TODO: Move logic to the protocol - // Set to true on fatal errors, to get this out of connection pool. this._isBroken = false @@ -167,7 +165,7 @@ export default class ChannelConnection extends Connection { } get supportsReAuth () { - return this._supportsReAuth + return this._protocol.supportsLogoff } get id () { @@ -188,15 +186,26 @@ export default class ChannelConnection extends Connection { * @param {Object} authToken the object containing auth information. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ - connect (userAgent, authToken) { + async connect (userAgent, authToken) { this._authToken = authToken - return this._initialize(userAgent, authToken) - } + if (!this._protocol.initialized) { + return await this._initialize(userAgent, authToken) + } - async reAuth (authToken) { - this._authToken = authToken - console.log('re aut') - return this + if (!this._protocol.supportsLogoff) { + throw newError('Connection does not support re-auth') + } + + const logoffPromise = new Promise((resolve, reject) => { + this._protocol.logoff({ onComplete: resolve, onError: reject }) + }) + + const loginPromise = new Promise((resolve, reject) => { + this._protocol.login({ onComplete: resolve, onError: reject, flush: true }) + }) + + return await Promise.all(logoffPromise, loginPromise) + .then(() => this) } /** diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js index 973c2a970..57813ac38 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js @@ -79,10 +79,6 @@ export default class DelegateConnection extends Connection { return this._delegate.isOpen() } - reAuth (authToken) { - return this._delegate.reAuth(authToken) - } - protocol () { return this._delegate.protocol() } From dab7f38b3a422778ed8592a5ebdbf69d05434783 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 11 Jan 2023 13:17:10 +0100 Subject: [PATCH 09/70] Add basic testkit tests for AuthProvider --- .../authentication-provider.js | 4 +- .../authentication-provider.js | 4 +- packages/neo4j-driver/src/index.js | 24 +++++-- packages/testkit-backend/src/context.js | 30 +++++++++ .../src/cypher-native-binders.js | 24 +++++++ .../src/request-handlers-rx.js | 5 +- .../testkit-backend/src/request-handlers.js | 64 ++++++++++++------- packages/testkit-backend/src/responses.js | 8 +++ 8 files changed, 129 insertions(+), 34 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index a76a8a5f1..dbb1e3525 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -33,7 +33,7 @@ export default class AuthenticationProvider { await this._getFreshAuthToken() } - if (this._renewableAuthToken.authToken !== connection.authToken || this._isTokenExpired()) { + if (this._renewableAuthToken.authToken !== connection.authToken || this._isTokenExpired) { return await connection.connect(this._userAgent, this._authToken) } @@ -68,7 +68,7 @@ export default class AuthenticationProvider { async _getFreshAuthToken () { if (this._isTokenExpired) { const promise = new Promise((resolve, reject) => { - this.scheduleRefresh({ + this._scheduleRefresh({ onSuccess: resolve, onError: reject }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index a76a8a5f1..dbb1e3525 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -33,7 +33,7 @@ export default class AuthenticationProvider { await this._getFreshAuthToken() } - if (this._renewableAuthToken.authToken !== connection.authToken || this._isTokenExpired()) { + if (this._renewableAuthToken.authToken !== connection.authToken || this._isTokenExpired) { return await connection.connect(this._userAgent, this._authToken) } @@ -68,7 +68,7 @@ export default class AuthenticationProvider { async _getFreshAuthToken () { if (this._isTokenExpired) { const promise = new Promise((resolve, reject) => { - this.scheduleRefresh({ + this._scheduleRefresh({ onSuccess: resolve, onError: reject }) diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 954bff531..d2261e1c9 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -91,6 +91,22 @@ const { urlUtil } = internal +function createAuthProvider (authTokenOrProvider) { + if (typeof authTokenOrProvider === 'function') { + return authTokenOrProvider + } + + let authToken = authTokenOrProvider + // Sanitize authority token. Nicer error from server when a scheme is set. + authToken = authToken ?? {} + authToken.scheme = authToken.scheme ?? 'none' + return function () { + return { + authToken + } + } +} + /** * Construct a new Neo4j Driver. This is your main entry point for this * library. @@ -273,9 +289,7 @@ function driver (url, authToken, config = {}) { config.trust = trust } - // Sanitize authority token. Nicer error from server when a scheme is set. - authToken = authToken || {} - authToken.scheme = authToken.scheme || 'none' + const authTokenProvider = createAuthProvider(authToken) // Use default user agent or user agent specified by user. config.userAgent = config.userAgent || USER_AGENT @@ -297,7 +311,7 @@ function driver (url, authToken, config = {}) { config, log, hostNameResolver, - authToken, + authTokenProvider, address, userAgent: config.userAgent, routingContext: parsedUrl.query @@ -314,7 +328,7 @@ function driver (url, authToken, config = {}) { id, config, log, - authToken, + authTokenProvider, address, userAgent: config.userAgent }) diff --git a/packages/testkit-backend/src/context.js b/packages/testkit-backend/src/context.js index ab4733c0b..56b65a280 100644 --- a/packages/testkit-backend/src/context.js +++ b/packages/testkit-backend/src/context.js @@ -12,6 +12,8 @@ export default class Context { this._bookmarkSupplierRequests = {} this._notifyBookmarksRequests = {} this._bookmarksManagers = {} + this._authTokenProviders = {} + this._authTokenProviderRequests = {} this._binder = binder this._environmentLogLevel = environmentLogLevel } @@ -161,6 +163,34 @@ export default class Context { delete this._bookmarksManagers[id] } + addAuthTokenProvider (authTokenProviderFactory) { + this._id++ + this._authTokenProviders[this._id] = authTokenProviderFactory(this._id) + return this._id + } + + getAuthTokenProvider (id) { + return this._authTokenProviders[id] + } + + removeAuthTokenProvider (id) { + delete this._authTokenProviders[id] + } + + addAuthTokenProviderRequest (resolve, reject) { + return this._add(this._authTokenProviderRequests, { + resolve, reject + }) + } + + removeAuthTokenProviderRequest (id) { + delete this._authTokenProviderRequests[id] + } + + getAuthTokenProviderRequest (id) { + return this._authTokenProviderRequests[id] + } + _add (map, object) { this._id++ map[this._id] = object diff --git a/packages/testkit-backend/src/cypher-native-binders.js b/packages/testkit-backend/src/cypher-native-binders.js index a0535732f..ff9498e7e 100644 --- a/packages/testkit-backend/src/cypher-native-binders.js +++ b/packages/testkit-backend/src/cypher-native-binders.js @@ -253,10 +253,34 @@ export default function CypherNativeBinders (neo4j) { throw Error(err) } + function parseAuthToken (authToken) { + switch (authToken.scheme) { + case 'basic': + return neo4j.auth.basic( + authToken.principal, + authToken.credentials, + authToken.realm + ) + case 'kerberos': + return neo4j.auth.kerberos(authToken.credentials) + case 'bearer': + return neo4j.auth.bearer(authToken.credentials) + default: + return neo4j.auth.custom( + authToken.principal, + authToken.credentials, + authToken.realm, + authToken.scheme, + authToken.parameters + ) + } + } + this.valueResponse = valueResponse this.objectToCypher = objectToCypher this.objectToNative = objectToNative this.objectMemberBitIntToNumber = objectMemberBitIntToNumber this.nativeToCypher = nativeToCypher this.cypherToNative = cypherToNative + this.parseAuthToken = parseAuthToken } diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 1d0762856..77ca14f11 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -22,7 +22,10 @@ export { BookmarksSupplierCompleted, BookmarksConsumerCompleted, StartSubTest, - ExecuteQuery + ExecuteQuery, + NewAuthTokenProvider, + AuthTokenProviderCompleted, + AuthTokenProviderClose } from './request-handlers.js' export function NewSession (neo4j, context, data, wire) { diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 295033053..e29278a62 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -11,33 +11,21 @@ export function isFrontendError (error) { export function NewDriver (neo4j, context, data, wire) { const { uri, - authorizationToken: { data: authToken }, + authorizationToken, + authTokenProviderId, userAgent, resolverRegistered } = data - let parsedAuthToken = authToken - switch (authToken.scheme) { - case 'basic': - parsedAuthToken = neo4j.auth.basic( - authToken.principal, - authToken.credentials, - authToken.realm - ) - break - case 'kerberos': - parsedAuthToken = neo4j.auth.kerberos(authToken.credentials) - break - case 'bearer': - parsedAuthToken = neo4j.auth.bearer(authToken.credentials) - break - default: - parsedAuthToken = neo4j.auth.custom( - authToken.principal, - authToken.credentials, - authToken.realm, - authToken.scheme, - authToken.parameters - ) + + let parsedAuthToken = null + + if (authorizationToken != null && authTokenProviderId != null) { + throw new Error('Can not set authorizationToken and authTokenProviderId') + } else if (authorizationToken) { + const { data: authToken } = authorizationToken + parsedAuthToken = context.binder.parseAuthToken(authToken) + } else { + parsedAuthToken = context.getAuthTokenProvider(authTokenProviderId) } const resolver = resolverRegistered @@ -506,6 +494,34 @@ export function BookmarksConsumerCompleted ( notifyBookmarksRequest.resolve() } +export function NewAuthTokenProvider (_, context, _data, wire) { + const id = context.addAuthTokenProvider(authTokenProviderId => { + return () => new Promise((resolve, reject) => { + const id = context.addAuthTokenProviderRequest(resolve, reject) + wire.writeResponse(responses.AuthTokenProviderRequest({ id, authTokenProviderId })) + }) + }) + + wire.writeResponse(responses.AuthTokenProvider({ id })) +} + +export function AuthTokenProviderCompleted (_, context, { requestId, auth }, _wire) { + const request = context.getAuthTokenProviderRequest(requestId) + const renewableToken = { + expectedExpirationTime: auth.data.expiresInMs != null + ? new Date(new Date().getUTCMilliseconds() + auth.data.expiresInMs) + : undefined, + authToken: context.binder.parseAuthToken(auth.data.auth.data) + } + request.resolve(renewableToken) + context.removeAuthTokenProviderRequest(requestId) +} + +export function AuthTokenProviderClose (_, context, { id }, wire) { + context.removeAuthTokenProvider(id) + wire.writeResponse(responses.AuthTokenProvider({ id })) +} + export function GetRoutingTable (_, context, { driverId, database }, wire) { const driver = context.getDriver(driverId) const routingTable = diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index 7278e9f0c..122bccf18 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -99,6 +99,14 @@ export function EagerResult ({ keys, records, summary }, { binder }) { }) } +export function AuthTokenProvider ({ id }) { + return response('AuthTokenProvider', { id }) +} + +export function AuthTokenProviderRequest ({ id, authTokenProviderId }) { + return response('AuthTokenProviderRequest', { id, authTokenProviderId }) +} + // Testkit controller messages export function RunTest () { return response('RunTest', null) From dbdc8ed147282987489af4ded67cd71f4f3fd85f Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 11 Jan 2023 18:55:34 +0100 Subject: [PATCH 10/70] Ajust the re-auth flow and support FakeTime in tk --- .../authentication-provider.js | 4 ++-- .../src/connection/connection-channel.js | 4 ++-- packages/bolt-connection/src/pool/pool.js | 8 ++++---- .../authentication-provider.js | 4 ++-- .../connection/connection-channel.js | 4 ++-- .../lib/bolt-connection/pool/pool.js | 8 ++++---- packages/testkit-backend/package.json | 3 ++- .../testkit-backend/src/request-handlers-rx.js | 6 +++++- .../testkit-backend/src/request-handlers.js | 18 +++++++++++++++++- packages/testkit-backend/src/responses.js | 4 ++++ 10 files changed, 44 insertions(+), 19 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index dbb1e3525..5ba818011 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -29,11 +29,11 @@ export default class AuthenticationProvider { } async authenticate ({ connection }) { - if (!this._authToken) { + if (!this._authToken || this._isTokenExpired) { await this._getFreshAuthToken() } - if (this._renewableAuthToken.authToken !== connection.authToken || this._isTokenExpired) { + if (this._renewableAuthToken.authToken !== connection.authToken) { return await connection.connect(this._userAgent, this._authToken) } diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 2aa2e7553..6f19478e7 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -201,10 +201,10 @@ export default class ChannelConnection extends Connection { }) const loginPromise = new Promise((resolve, reject) => { - this._protocol.login({ onComplete: resolve, onError: reject, flush: true }) + this._protocol.login({ onComplete: resolve, onError: reject, authToken, flush: true }) }) - return await Promise.all(logoffPromise, loginPromise) + return await Promise.all([logoffPromise, loginPromise]) .then(() => this) } diff --git a/packages/bolt-connection/src/pool/pool.js b/packages/bolt-connection/src/pool/pool.js index 3f7199589..242543c06 100644 --- a/packages/bolt-connection/src/pool/pool.js +++ b/packages/bolt-connection/src/pool/pool.js @@ -203,11 +203,11 @@ class Pool { while (pool.length) { const resource = pool.pop() - if (await this._validate(resource)) { - if (this._removeIdleObserver) { - this._removeIdleObserver(resource) - } + if (this._removeIdleObserver) { + this._removeIdleObserver(resource) + } + if (await this._validate(resource)) { // idle resource is valid and can be acquired resourceAcquired(key, this._activeResourceCounts) if (this._log.isDebugEnabled()) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index dbb1e3525..5ba818011 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -29,11 +29,11 @@ export default class AuthenticationProvider { } async authenticate ({ connection }) { - if (!this._authToken) { + if (!this._authToken || this._isTokenExpired) { await this._getFreshAuthToken() } - if (this._renewableAuthToken.authToken !== connection.authToken || this._isTokenExpired) { + if (this._renewableAuthToken.authToken !== connection.authToken) { return await connection.connect(this._userAgent, this._authToken) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index bf8357f5a..8ec505aa9 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -201,10 +201,10 @@ export default class ChannelConnection extends Connection { }) const loginPromise = new Promise((resolve, reject) => { - this._protocol.login({ onComplete: resolve, onError: reject, flush: true }) + this._protocol.login({ onComplete: resolve, onError: reject, authToken, flush: true }) }) - return await Promise.all(logoffPromise, loginPromise) + return await Promise.all([logoffPromise, loginPromise]) .then(() => this) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js index ec14ce0ee..9519cff72 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js @@ -203,11 +203,11 @@ class Pool { while (pool.length) { const resource = pool.pop() - if (await this._validate(resource)) { - if (this._removeIdleObserver) { - this._removeIdleObserver(resource) - } + if (this._removeIdleObserver) { + this._removeIdleObserver(resource) + } + if (await this._validate(resource)) { // idle resource is valid and can be acquired resourceAcquired(key, this._activeResourceCounts) if (this._log.isDebugEnabled()) { diff --git a/packages/testkit-backend/package.json b/packages/testkit-backend/package.json index e10c21db7..923ce63e0 100644 --- a/packages/testkit-backend/package.json +++ b/packages/testkit-backend/package.json @@ -43,6 +43,7 @@ "esm": "^3.2.25", "rollup": "^2.77.4-1", "rollup-plugin-inject-process-env": "^1.3.1", - "rollup-plugin-polyfill-node": "^0.11.0" + "rollup-plugin-polyfill-node": "^0.11.0", + "sinon": "^15.0.1" } } diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 77ca14f11..6e1879e10 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -25,7 +25,11 @@ export { ExecuteQuery, NewAuthTokenProvider, AuthTokenProviderCompleted, - AuthTokenProviderClose + AuthTokenProviderClose, + StartSubTest, + FakeTimeInstall, + FakeTimeTick, + FakeTimeUninstall } from './request-handlers.js' export function NewSession (neo4j, context, data, wire) { diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index e29278a62..4a934ac19 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -1,4 +1,5 @@ import * as responses from './responses.js' +import sinon from 'sinon' export function throwFrontendError () { throw new Error('TestKit FrontendError') @@ -509,7 +510,7 @@ export function AuthTokenProviderCompleted (_, context, { requestId, auth }, _wi const request = context.getAuthTokenProviderRequest(requestId) const renewableToken = { expectedExpirationTime: auth.data.expiresInMs != null - ? new Date(new Date().getUTCMilliseconds() + auth.data.expiresInMs) + ? new Date(new Date().getTime() + auth.data.expiresInMs) : undefined, authToken: context.binder.parseAuthToken(auth.data.auth.data) } @@ -617,3 +618,18 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config }) .catch(e => wire.writeError(e)) } + +export function FakeTimeInstall (_, context, _data, wire) { + context.clock = sinon.useFakeTimers(new Date().getTime()) + wire.writeResponse(responses.FakeTimeAck()) +} + +export function FakeTimeTick (_, context, { incrementMs }, wire) { + context.clock.tick(incrementMs) + wire.writeResponse(responses.FakeTimeAck()) +} + +export function FakeTimeUninstall (_, context, _data, wire) { + context.clock.restore() + wire.writeResponse(responses.FakeTimeAck()) +} diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index 122bccf18..f6d6a6e33 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -124,6 +124,10 @@ export function FeatureList ({ features }) { return response('FeatureList', { features }) } +export function FakeTimeAck () { + return response('FakeTimeAck', {}) +} + function response (name, data) { return { name, data } } From 89e27d34cf66bd3c25e7930ac74b2c6678168915 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Fri, 13 Jan 2023 14:44:49 +0100 Subject: [PATCH 11/70] Session config --- .../connection-provider-direct.js | 24 ++++++++++++------- .../testkit-backend/src/feature/common.js | 1 + .../testkit-backend/src/request-handlers.js | 10 +++++++- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 3d81afe60..aab5c63bd 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -42,28 +42,34 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - acquireConnection ({ accessMode, database, bookmarks } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, auth } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn, database) }) - return this._connectionPool - .acquire(this._address) - .then( - connection => - new DelegateConnection(connection, databaseSpecificErrorHandler) - ) + const connection = await this._connectionPool.acquire(this._address) + + if (auth && auth !== connection.authToken) { + if (connection.supportsReAuth) { + await connection.connect(this._userAgent, auth) + } else { + await connection._release() + return await this._createStickyConnection({ address: this._address, auth }) + } + } + + return new DelegateConnection(connection, databaseSpecificErrorHandler) } _handleAuthorizationExpired (error, address, connection, database) { this._log.warn( `Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - + this._authenticationProvider.handleError({ connection, code: error.code }) - + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { this._connectionPool.apply(address, (conn) => conn.authToken === null) } diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 99181a219..ff844f914 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -4,6 +4,7 @@ const features = [ 'Feature:Auth:Kerberos', 'Feature:Auth:Bearer', 'Feature:API:BookmarkManager', + 'Feature:API:Session:AuthConfig', 'Feature:API:SSLConfig', 'Feature:API:SSLSchemes', 'Feature:API:Type.Temporal', diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 4a934ac19..5103245dc 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -122,6 +122,7 @@ export function NewSession (neo4j, context, data, wire) { return } } +<<<<<<< HEAD let notificationFilter if ('notificationsMinSeverity' in data || 'notificationsDisabledCategories' in data) { notificationFilter = { @@ -129,6 +130,12 @@ export function NewSession (neo4j, context, data, wire) { disabledCategories: data.notificationsDisabledCategories } } +======= + const auth = data.authorizationToken != null + ? context.binder.parseAuthToken(data.authorizationToken.data) + : undefined + +>>>>>>> 7fa91180 (Session config) const driver = context.getDriver(driverId) const session = driver.session({ defaultAccessMode: accessMode, @@ -137,7 +144,8 @@ export function NewSession (neo4j, context, data, wire) { fetchSize, impersonatedUser, bookmarkManager, - notificationFilter + notificationFilter, + auth }) const id = context.addSession(session) wire.writeResponse(responses.Session({ id })) From f5f66c82aa249d89ccb5d5a44bc99a6bed89f4ff Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Fri, 13 Jan 2023 17:49:25 +0100 Subject: [PATCH 12/70] Pool: separate validation on acquire and on release and add the acquisition context to create and validate --- .../authentication-provider.js | 11 +- .../connection-provider-direct.js | 13 +- .../connection-provider-pooled.js | 49 ++- .../connection-provider-routing.js | 51 +--- packages/bolt-connection/src/lang/index.js | 1 + packages/bolt-connection/src/lang/object.js | 43 +++ packages/bolt-connection/src/pool/pool.js | 41 ++- .../connection-provider-direct.test.js | 2 +- .../connection-provider-routing.test.js | 16 +- .../bolt-connection/test/pool/pool.test.js | 279 +++++++++--------- .../authentication-provider.js | 11 +- .../connection-provider-direct.js | 21 +- .../connection-provider-pooled.js | 49 ++- .../connection-provider-routing.js | 51 +--- .../lib/bolt-connection/lang/index.js | 1 + .../lib/bolt-connection/lang/object.js | 43 +++ .../lib/bolt-connection/pool/pool.js | 41 ++- .../testkit-backend/src/feature/common.js | 1 + 18 files changed, 420 insertions(+), 304 deletions(-) create mode 100644 packages/bolt-connection/src/lang/object.js create mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/lang/object.js diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index 5ba818011..d35d93991 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -17,6 +17,8 @@ * limitations under the License. */ +import { object } from '../lang' + /** * Class which provides Authorization for {@link Connection} */ @@ -28,7 +30,14 @@ export default class AuthenticationProvider { this._refreshObserver = undefined } - async authenticate ({ connection }) { + async authenticate ({ connection, auth }) { + if (auth != null) { + if (connection.authToken == null || (connection.supportsReAuth && !object.equals(connection.authToken, auth))) { + return await connection.connect(this._userAgent, auth) + } + return connection + } + if (!this._authToken || this._isTokenExpired) { await this._getFreshAuthToken() } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index aab5c63bd..f5ab8c7ae 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -24,6 +24,7 @@ import { ConnectionErrorHandler } from '../connection' import { internal, error } from 'neo4j-driver-core' +import { object } from '../lang' const { constants: { BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, BOLT_PROTOCOL_V4_4 } @@ -49,15 +50,11 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { this._handleAuthorizationExpired(error, address, conn, database) }) - const connection = await this._connectionPool.acquire(this._address) + const connection = await this._connectionPool.acquire({ auth }, this._address) - if (auth && auth !== connection.authToken) { - if (connection.supportsReAuth) { - await connection.connect(this._userAgent, auth) - } else { - await connection._release() - return await this._createStickyConnection({ address: this._address, auth }) - } + if (auth && !object.equals(auth, connection.authToken)) { + await connection._release() + return await this._createStickyConnection({ address: this._address, auth }) } return new DelegateConnection(connection, databaseSpecificErrorHandler) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 974e01d63..060be7b57 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -47,7 +47,8 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._connectionPool = new Pool({ create: this._createConnection.bind(this), destroy: this._destroyConnection.bind(this), - validate: this._validateConnection.bind(this), + validateOnAcquire: this._validateConnectionOnAcquire.bind(this), + validateOnRelease: this._validateConnectionOnRelease.bind(this), installIdleObserver: PooledConnectionProvider._installIdleObserverOnConnection.bind( this ), @@ -69,13 +70,13 @@ export default class PooledConnectionProvider extends ConnectionProvider { * @return {Promise} promise resolved with a new connection or rejected when failed to connect. * @access private */ - _createConnection (address, release) { + _createConnection ({ auth }, address, release) { return this._createChannelConnection(address).then(connection => { connection._release = () => { return release(address, connection) } this._openConnections[connection.id] = connection - return this._authenticationProvider.authenticate({ connection }) + return this._authenticationProvider.authenticate({ connection, auth }) .catch(error => { // let's destroy this connection this._destroyConnection(connection) @@ -85,12 +86,32 @@ export default class PooledConnectionProvider extends ConnectionProvider { }) } + async _validateConnectionOnAcquire ({ auth }, conn) { + if (!this._validateConnection(conn)) { + return false + } + + try { + await this._authenticationProvider.authenticate({ connection: conn, auth }) + return true + } catch (error) { + this._log.info( + `The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'` + ) + return false + } + } + + _validateConnectionOnRelease (conn) { + return this._validateConnection(conn) + } + /** * Check that a connection is usable * @return {boolean} true if the connection is open * @access private **/ - async _validateConnection (conn) { + _validateConnection (conn) { if (!conn.isOpen()) { return false } @@ -101,16 +122,20 @@ export default class PooledConnectionProvider extends ConnectionProvider { return false } + return true + } + + async _createStickyConnection ({ address, auth }) { + const connection = this._createChannelConnection(address) + connection._release = () => this._destroyConnection(connection) + this._openConnections[connection.id] = connection + try { - await this._authenticationProvider.authenticate({ connection: conn }) + return await connection.connect(this._userAgent, auth) } catch (error) { - this._log.info( - `The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'` - ) - return false + await this._destroyConnection() + throw error } - - return true } /** @@ -130,7 +155,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { * @return {Promise} the server info */ async _verifyConnectivityAndGetServerVersion ({ address }) { - const connection = await this._connectionPool.acquire(address) + const connection = await this._connectionPool.acquire({}, address) const serverInfo = new ServerInfo(connection.server, connection.protocol().version) try { if (!connection.protocol().isLastMessageLogon()) { diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 7db1c2383..ecbbfd8ef 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -28,6 +28,7 @@ import { ConnectionErrorHandler, DelegateConnection } from '../connection' +import { object } from '../lang' const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error const { @@ -186,19 +187,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider } try { - const connection = await this._acquireConnectionToServer( - address, - name, - routingTable - ) + const connection = await this._connectionPool.acquire({ auth }, address) if (auth && auth !== connection.authToken) { - if (connection.supportsReAuth) { - await connection.connect(this._userAgent, auth) - } else { - await connection._release() - return await this._createStickyConnection({ address, auth }) - } + await connection._release() + return await this._createStickyConnection({ address, auth }) } return new DelegateConnection(connection, databaseSpecificErrorHandler) @@ -316,10 +309,6 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - _acquireConnectionToServer (address, serverName, routingTable) { - return this._connectionPool.acquire(address) - } - _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { const currentRoutingTable = this._routingTableRegistry.get( database, @@ -544,33 +533,16 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - async _createStickyConnection ({ address, auth }) { - const connection = await this._createChannelConnection(address) - connection._release = () => this._destroyConnection(connection) - this._openConnections[connection.id] = connection - - try { - return await connection.connect(this._userAgent, auth) - } catch (error) { - await this._destroyConnection() - throw error - } - } - async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth) { try { - let connection = await this._connectionPool.acquire(routerAddress) + let connection = await this._connectionPool.acquire({ auth }, routerAddress) - if (auth && connection.authToken !== auth) { - if (connection.supportsReAuth) { - await await connection.connect(this._userAgent, auth) - } else { - await connection._release() - connection = await this._createStickyConnection({ - address: routerAddress, - auth - }) - } + if (auth && object.equals(auth, connection.authToken)) { + await connection._release() + connection = await this._createStickyConnection({ + address: routerAddress, + auth + }) } const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ @@ -767,7 +739,6 @@ function _isFailFastError (error) { } function _isFailFastSecurityError (error) { - console.error(error) return error.code.startsWith('Neo.ClientError.Security.') && ![ AUTHORIZATION_EXPIRED_CODE diff --git a/packages/bolt-connection/src/lang/index.js b/packages/bolt-connection/src/lang/index.js index 9d565fe8f..ab67c87c5 100644 --- a/packages/bolt-connection/src/lang/index.js +++ b/packages/bolt-connection/src/lang/index.js @@ -18,3 +18,4 @@ */ export * as functional from './functional' +export * as object from './object' diff --git a/packages/bolt-connection/src/lang/object.js b/packages/bolt-connection/src/lang/object.js new file mode 100644 index 000000000..a8d7f29cb --- /dev/null +++ b/packages/bolt-connection/src/lang/object.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function equals (a, b) { + if (a === b) { + return true + } + + if (typeof a === 'object' && typeof b === 'object') { + const keysA = Object.keys(a) + const keysB = Object.keys(b) + + if (keysA.length !== keysB.length) { + return false + } + + for (const key of keysA) { + if (a[key] !== b[key]) { + return false + } + } + + return true + } + + return false +} diff --git a/packages/bolt-connection/src/pool/pool.js b/packages/bolt-connection/src/pool/pool.js index 242543c06..0bf53dd32 100644 --- a/packages/bolt-connection/src/pool/pool.js +++ b/packages/bolt-connection/src/pool/pool.js @@ -26,15 +26,18 @@ const { class Pool { /** - * @param {function(address: ServerAddress, function(address: ServerAddress, resource: object): Promise): Promise} create + * @param {function(acquisitionContext: object, address: ServerAddress, function(address: ServerAddress, resource: object): Promise): Promise} create * an allocation function that creates a promise with a new resource. It's given an address for which to * allocate the connection and a function that will return the resource to the pool if invoked, which is * meant to be called on .dispose or .close or whatever mechanism the resource uses to finalize. + * @param {function(acquisitionContext: object, resource: object): boolean} validateOnAcquire + * called at various times when an instance is acquired + * If this returns false, the resource will be evicted + * @param {function(resource: object): boolean} validateOnRelease + * called at various times when an instance is released + * If this returns false, the resource will be evicted * @param {function(resource: object): Promise} destroy * called with the resource when it is evicted from this pool - * @param {function(resource: object): boolean} validate - * called at various times (like when an instance is acquired and when it is returned. - * If this returns false, the resource will be evicted * @param {function(resource: object, observer: { onError }): void} installIdleObserver * called when the resource is released back to pool * @param {function(resource: object): void} removeIdleObserver @@ -43,9 +46,10 @@ class Pool { * @param {Logger} log the driver logger. */ constructor ({ - create = (address, release) => Promise.resolve(), + create = (acquisitionContext, address, release) => Promise.resolve(), destroy = conn => Promise.resolve(), - validate = conn => true, + validateOnAcquire = (acquisitionContext, conn) => true, + validateOnRelease = (conn) => true, installIdleObserver = (conn, observer) => {}, removeIdleObserver = conn => {}, config = PoolConfig.defaultConfig(), @@ -53,7 +57,8 @@ class Pool { } = {}) { this._create = create this._destroy = destroy - this._validate = validate + this._validateOnAcquire = validateOnAcquire + this._validateOnRelease = validateOnRelease this._installIdleObserver = installIdleObserver this._removeIdleObserver = removeIdleObserver this._maxSize = config.maxSize @@ -69,10 +74,11 @@ class Pool { /** * Acquire and idle resource fom the pool or create a new one. + * @param {object} acquisitionContext the acquisition context used for create and validateOnAcquire connection * @param {ServerAddress} address the address for which we're acquiring. * @return {Promise} resource that is ready to use. */ - acquire (address) { + acquire (acquisitionContext, address) { const key = address.asKey() // We're out of resources and will try to acquire later on when an existing resource is released. @@ -108,7 +114,7 @@ class Pool { } }, this._acquisitionTimeout) - request = new PendingRequest(key, resolve, reject, timeoutId, this._log) + request = new PendingRequest(key, acquisitionContext, resolve, reject, timeoutId, this._log) allRequests[key].push(request) this._processPendingAcquireRequests(address) }) @@ -193,7 +199,7 @@ class Pool { return pool } - async _acquire (address) { + async _acquire (acquisitionContext, address) { if (this._closed) { throw newError('Pool is closed, it is no more able to serve requests.') } @@ -207,7 +213,7 @@ class Pool { this._removeIdleObserver(resource) } - if (await this._validate(resource)) { + if (await this._validateOnAcquire(acquisitionContext, resource)) { // idle resource is valid and can be acquired resourceAcquired(key, this._activeResourceCounts) if (this._log.isDebugEnabled()) { @@ -238,7 +244,7 @@ class Pool { let resource try { // Invoke callback that creates actual connection - resource = await this._create(address, (address, resource) => this._release(address, resource, pool)) + resource = await this._create(acquisitionContext, address, (address, resource) => this._release(address, resource, pool)) pool.pushInUse(resource) resourceAcquired(key, this._activeResourceCounts) @@ -256,7 +262,7 @@ class Pool { if (pool.isActive()) { // there exist idle connections for the given key - if (!await this._validate(resource)) { + if (!await this._validateOnRelease(resource)) { if (this._log.isDebugEnabled()) { this._log.debug( `${resource} destroyed and can't be released to the pool ${key} because it is not functional` @@ -327,7 +333,7 @@ class Pool { const pendingRequest = requests.shift() // pop a pending acquire request if (pendingRequest) { - this._acquire(address) + this._acquire(pendingRequest.context, address) .catch(error => { // failed to acquire/create a new connection to resolve the pending acquire request // propagate the error by failing the pending request @@ -391,8 +397,9 @@ function resourceReleased (key, activeResourceCounts) { } class PendingRequest { - constructor (key, resolve, reject, timeoutId, log) { + constructor (key, context, resolve, reject, timeoutId, log) { this._key = key + this._context = context this._resolve = resolve this._reject = reject this._timeoutId = timeoutId @@ -400,6 +407,10 @@ class PendingRequest { this._completed = false } + get context () { + return this._context + } + isCompleted () { return this._completed } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index ef717f762..d171fc89b 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -333,7 +333,7 @@ function newPool ({ create, config } = {}) { } return new Pool({ config, - create: (address, release) => + create: (_, address, release) => Promise.resolve(_create(address, release)) }) } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 91994f78b..5626cda28 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -129,9 +129,9 @@ describe.each([ it('purges connections when address is forgotten', () => { const pool = newPool() - pool.acquire(server1) - pool.acquire(server3) - pool.acquire(server5) + pool.acquire({}, server1) + pool.acquire({}, server3) + pool.acquire({}, server5) expectPoolToContain(pool, [server1, server3, server5]) const connectionProvider = newRoutingConnectionProvider( @@ -2589,7 +2589,7 @@ describe.each([ const targetServers = accessMode === WRITE ? routingTable.writers : routingTable.readers const address = targetServers[0] - expect(acquireSpy).toHaveBeenCalledWith(address) + expect(acquireSpy).toHaveBeenCalledWith({}, address) const connections = seenConnectionsPerAddress.get(address) @@ -2608,7 +2608,7 @@ describe.each([ const targetServers = accessMode === WRITE ? routingTable.writers : routingTable.readers const address = targetServers[0] - expect(acquireSpy).toHaveBeenCalledWith(address) + expect(acquireSpy).toHaveBeenCalledWith({}, address) const connections = seenConnectionsPerAddress.get(address) @@ -2628,7 +2628,7 @@ describe.each([ const targetServers = accessMode === WRITE ? routingTable.readers : routingTable.writers for (const address of targetServers) { - expect(acquireSpy).not.toHaveBeenCalledWith(address) + expect(acquireSpy).not.toHaveBeenCalledWith({}, address) expect(seenConnectionsPerAddress.get(address)).toBeUndefined() } }) @@ -2711,7 +2711,7 @@ describe.each([ } finally { const targetServers = accessMode === WRITE ? routingTable.writers : routingTable.readers for (const address of targetServers) { - expect(acquireSpy).toHaveBeenCalledWith(address) + expect(acquireSpy).toHaveBeenCalledWith({}, address) const connections = seenConnectionsPerAddress.get(address) @@ -2947,7 +2947,7 @@ describe.each([ } return new Pool({ config, - create: (address, release) => _create(address, release) + create: (_, address, release) => _create(address, release) }) } diff --git a/packages/bolt-connection/test/pool/pool.test.js b/packages/bolt-connection/test/pool/pool.test.js index 2037cd8b5..48d20c0eb 100644 --- a/packages/bolt-connection/test/pool/pool.test.js +++ b/packages/bolt-connection/test/pool/pool.test.js @@ -33,13 +33,13 @@ describe('#unit Pool', () => { let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)) }) // When - const r0 = await pool.acquire(address) - const r1 = await pool.acquire(address) + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) // Then expect(r0.id).toBe(0) @@ -52,15 +52,15 @@ describe('#unit Pool', () => { let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)) }) // When - const r0 = await pool.acquire(address) + const r0 = await pool.acquire({}, address) await r0.close() - const r1 = await pool.acquire(address) + const r1 = await pool.acquire({}, address) // Then expect(r0.id).toBe(0) @@ -74,17 +74,17 @@ describe('#unit Pool', () => { const address1 = ServerAddress.fromUrl('bolt://localhost:7687') const address2 = ServerAddress.fromUrl('bolt://localhost:7688') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)) }) // When - const r0 = await pool.acquire(address1) - const r1 = await pool.acquire(address2) + const r0 = await pool.acquire({}, address1) + const r1 = await pool.acquire({}, address2) await r0.close() - const r2 = await pool.acquire(address1) - const r3 = await pool.acquire(address2) + const r2 = await pool.acquire({}, address1) + const r3 = await pool.acquire({}, address2) // Then expect(r0.id).toBe(0) @@ -102,19 +102,19 @@ describe('#unit Pool', () => { const destroyed = [] const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_acquisitionContext, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { destroyed.push(res) return Promise.resolve() }, - validate: res => false, + validateOnRelease: res => false, config: new PoolConfig(1000, 60000) }) // When - const r0 = await pool.acquire(address) - const r1 = await pool.acquire(address) + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) // Then await r0.close() @@ -131,7 +131,7 @@ describe('#unit Pool', () => { const address1 = ServerAddress.fromUrl('bolt://localhost:7687') const address2 = ServerAddress.fromUrl('bolt://localhost:7688') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true @@ -140,8 +140,8 @@ describe('#unit Pool', () => { }) // When - const r0 = await pool.acquire(address1) - const r1 = await pool.acquire(address2) + const r0 = await pool.acquire({}, address1) + const r1 = await pool.acquire({}, address2) await r0.close() await r1.close() @@ -154,8 +154,8 @@ describe('#unit Pool', () => { expect(pool.has(address1)).toBeFalsy() expect(pool.has(address2)).toBeTruthy() - const r2 = await pool.acquire(address1) - const r3 = await pool.acquire(address2) + const r2 = await pool.acquire({}, address1) + const r3 = await pool.acquire({}, address2) // Then expect(r0.id).toBe(0) @@ -171,7 +171,7 @@ describe('#unit Pool', () => { const address1 = ServerAddress.fromUrl('bolt://localhost:7687') const address2 = ServerAddress.fromUrl('bolt://localhost:7688') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true @@ -180,11 +180,11 @@ describe('#unit Pool', () => { }) // When - const r00 = await pool.acquire(address1) - const r01 = await pool.acquire(address1) - await pool.acquire(address2) - await pool.acquire(address2) - await pool.acquire(address2) + const r00 = await pool.acquire({}, address1) + const r01 = await pool.acquire({}, address1) + await pool.acquire({}, address2) + await pool.acquire({}, address2) + await pool.acquire({}, address2) expect(pool.activeResourceCount(address1)).toEqual(2) expect(pool.activeResourceCount(address2)).toEqual(3) @@ -216,7 +216,7 @@ describe('#unit Pool', () => { let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true @@ -224,7 +224,7 @@ describe('#unit Pool', () => { } }) - const r0 = await pool.acquire(address) + const r0 = await pool.acquire({}, address) expect(pool.has(address)).toBeTruthy() expect(r0.id).toEqual(0) @@ -241,7 +241,7 @@ describe('#unit Pool', () => { let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true @@ -250,7 +250,7 @@ describe('#unit Pool', () => { }) // Acquire resource - const r0 = await pool.acquire(address) + const r0 = await pool.acquire({}, address) expect(pool.has(address)).toBeTruthy() expect(r0.id).toEqual(0) @@ -260,7 +260,7 @@ describe('#unit Pool', () => { expect(r0.destroyed).toBeFalsy() // Acquiring second resource should recreate the pool - const r1 = await pool.acquire(address) + const r1 = await pool.acquire({}, address) expect(pool.has(address)).toBeTruthy() expect(r1.id).toEqual(1) @@ -283,7 +283,7 @@ describe('#unit Pool', () => { const address3 = ServerAddress.fromUrl('bolt://localhost:7689') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true @@ -292,12 +292,12 @@ describe('#unit Pool', () => { }) const acquiredResources = [ - pool.acquire(address1), - pool.acquire(address2), - pool.acquire(address3), - pool.acquire(address1), - pool.acquire(address2), - pool.acquire(address3) + pool.acquire({}, address2), + pool.acquire({}, address3), + pool.acquire({}, address1), + pool.acquire({}, address1), + pool.acquire({}, address2), + pool.acquire({}, address3) ] const values = await Promise.all(acquiredResources) await Promise.all(values.map(resource => resource.close())) @@ -310,7 +310,7 @@ describe('#unit Pool', () => { it('should fail to acquire when closed', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, 0, release)), destroy: res => { return Promise.resolve() @@ -320,7 +320,7 @@ describe('#unit Pool', () => { // Close the pool await pool.close() - await expect(pool.acquire(address)).rejects.toMatchObject({ + await expect(pool.acquire({}, address)).rejects.toMatchObject({ message: expect.stringMatching('Pool is closed') }) }) @@ -329,7 +329,7 @@ describe('#unit Pool', () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, 0, release)), destroy: res => { return Promise.resolve() @@ -337,13 +337,13 @@ describe('#unit Pool', () => { }) // Acquire and release a resource - const resource = await pool.acquire(address) + const resource = await pool.acquire({}, address) await resource.close() // Close the pool await pool.close() - await expect(pool.acquire(address)).rejects.toMatchObject({ + await expect(pool.acquire({}, address)).rejects.toMatchObject({ message: expect.stringMatching('Pool is closed') }) }) @@ -355,7 +355,7 @@ describe('#unit Pool', () => { const address3 = ServerAddress.fromUrl('bolt://localhost:7689') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true @@ -364,12 +364,12 @@ describe('#unit Pool', () => { }) const acquiredResources = [ - pool.acquire(address1), - pool.acquire(address2), - pool.acquire(address3), - pool.acquire(address1), - pool.acquire(address2), - pool.acquire(address3) + pool.acquire({}, address1), + pool.acquire({}, address2), + pool.acquire({}, address3), + pool.acquire({}, address1), + pool.acquire({}, address2), + pool.acquire({}, address3) ] await Promise.all(acquiredResources) @@ -392,7 +392,7 @@ describe('#unit Pool', () => { const address3 = ServerAddress.fromUrl('bolt://localhost:7689') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true @@ -401,12 +401,12 @@ describe('#unit Pool', () => { }) const acquiredResources = [ - pool.acquire(address1), - pool.acquire(address2), - pool.acquire(address3), - pool.acquire(address1), - pool.acquire(address2), - pool.acquire(address3) + pool.acquire({}, address1), + pool.acquire({}, address2), + pool.acquire({}, address3), + pool.acquire({}, address1), + pool.acquire({}, address2), + pool.acquire({}, address3) ] await Promise.all(acquiredResources) @@ -422,28 +422,28 @@ describe('#unit Pool', () => { }) it('skips broken connections during acquire', async () => { - let validated = false + let validated = true let counter = 0 const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => { res.destroyed = true return Promise.resolve() }, - validate: () => { - if (validated) { - return false + validateOnAcquire: (context, _res) => { + if (context.triggerValidation) { + validated = !validated + return validated } - validated = true return true } }) - const r0 = await pool.acquire(address) + const r0 = await pool.acquire({ triggerValidation: false }, address) await r0.close() - const r1 = await pool.acquire(address) + const r1 = await pool.acquire({ triggerValidation: true }, address) expect(r1).not.toBe(r0) }) @@ -453,12 +453,12 @@ describe('#unit Pool', () => { const absentAddress = ServerAddress.fromUrl('bolt://localhost:7688') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, 42, release)) }) - await pool.acquire(existingAddress) - await pool.acquire(existingAddress) + await pool.acquire({}, existingAddress) + await pool.acquire({}, existingAddress) expect(pool.has(existingAddress)).toBeTruthy() expect(pool.has(absentAddress)).toBeFalsy() @@ -466,7 +466,7 @@ describe('#unit Pool', () => { it('reports zero active resources when empty', () => { const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, 42, release)) }) @@ -484,14 +484,14 @@ describe('#unit Pool', () => { it('reports active resources', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, 42, release)) }) const acquiredResources = [ - pool.acquire(address), - pool.acquire(address), - pool.acquire(address) + pool.acquire({}, address), + pool.acquire({}, address), + pool.acquire({}, address) ] const values = await Promise.all(acquiredResources) @@ -503,21 +503,21 @@ describe('#unit Pool', () => { it('reports active resources when they are acquired', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, 42, release)) }) // three new resources are created and returned to the pool - const r0 = await pool.acquire(address) - const r1 = await pool.acquire(address) - const r2 = await pool.acquire(address) + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) + const r2 = await pool.acquire({}, address) await [r0, r1, r2].map(v => v.close()) // three idle resources are acquired from the pool const acquiredResources = [ - pool.acquire(address), - pool.acquire(address), - pool.acquire(address) + pool.acquire({}, address), + pool.acquire({}, address), + pool.acquire({}, address) ] const resources = await Promise.all(acquiredResources) @@ -531,13 +531,13 @@ describe('#unit Pool', () => { it('does not report resources that are returned to the pool', async () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, 42, release)) }) - const r0 = await pool.acquire(address) - const r1 = await pool.acquire(address) - const r2 = await pool.acquire(address) + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) + const r2 = await pool.acquire({}, address) expect(pool.activeResourceCount(address)).toEqual(3) await r0.close() @@ -549,7 +549,7 @@ describe('#unit Pool', () => { await r2.close() expect(pool.activeResourceCount(address)).toEqual(0) - const r3 = await pool.acquire(address) + const r3 = await pool.acquire({}, address) expect(pool.activeResourceCount(address)).toEqual(1) await r3.close() @@ -561,22 +561,21 @@ describe('#unit Pool', () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => Promise.resolve(), - validate: res => true, config: new PoolConfig(2, 5000) }) - await pool.acquire(address) - const r1 = await pool.acquire(address) + await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) setTimeout(() => { expectNumberOfAcquisitionRequests(pool, address, 1) r1.close() }, 1000) - const r2 = await pool.acquire(address) + const r2 = await pool.acquire({}, address) expect(r2).toBe(r1) }) @@ -585,17 +584,16 @@ describe('#unit Pool', () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => Promise.resolve(), - validate: res => true, config: new PoolConfig(2, 1000) }) - await pool.acquire(address) - await pool.acquire(address) + await pool.acquire({}, address) + await pool.acquire({}, address) - await expect(pool.acquire(address)).rejects.toMatchObject({ + await expect(pool.acquire({}, address)).rejects.toMatchObject({ message: expect.stringMatching('acquisition timed out') }) expectNumberOfAcquisitionRequests(pool, address, 0) @@ -608,7 +606,7 @@ describe('#unit Pool', () => { const pool = new Pool({ // Hook into connection creation to track when and what connections that are // created. - create: (server, release) => { + create: (_, server, release) => { // Create a fake connection that makes it possible control when it's connected // and released from the outer scope. const conn = { @@ -630,13 +628,13 @@ describe('#unit Pool', () => { // Make the first request for a connection, this will be hanging waiting for the // connect promise to be resolved. - const req1 = pool.acquire(address) + const req1 = pool.acquire({}, address) expect(conns.length).toEqual(1) // Make another request to the same server, this should not try to acquire another // connection since the pool will be full when the connection for the first request // is resolved. - const req2 = pool.acquire(address) + const req2 = pool.acquire({}, address) expect(conns.length).toEqual(1) // Let's fulfill the connect promise belonging to the first request. @@ -655,16 +653,15 @@ describe('#unit Pool', () => { let counter = 0 const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), - destroy: res => Promise.resolve(), - validate: res => true + destroy: res => Promise.resolve() }) - await pool.acquire(address) - await pool.acquire(address) + await pool.acquire({}, address) + await pool.acquire({}, address) - const r2 = await pool.acquire(address) + const r2 = await pool.acquire({}, address) expect(r2.id).toEqual(2) expectNoPendingAcquisitionRequests(pool) }) @@ -675,17 +672,16 @@ describe('#unit Pool', () => { const address = ServerAddress.fromUrl('bolt://localhost:7687') const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => Promise.resolve(), - validate: res => true, config: new PoolConfig(2, acquisitionTimeout) }) - const resource1 = await pool.acquire(address) + const resource1 = await pool.acquire({}, address) expect(resource1.id).toEqual(0) - const resource2 = await pool.acquire(address) + const resource2 = await pool.acquire({}, address) expect(resource2.id).toEqual(1) // try to release both resources around the time acquisition fails with timeout @@ -699,7 +695,7 @@ describe('#unit Pool', () => { // Remember that both code paths are ok with this test, either a success with a valid resource // or a time out error due to acquisition timeout being kicked in. await pool - .acquire(address) + .acquire({}, address) .then(someResource => { expect(someResource).toBeDefined() expect(someResource).not.toBeNull() @@ -718,14 +714,15 @@ describe('#unit Pool', () => { let counter = 0 const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => Promise.resolve(), - validate: resourceValidOnlyOnceValidationFunction, + validateOnAcquire: (_, res) => resourceValidOnlyOnceValidationFunction(res), + validateOnRelease: resourceValidOnlyOnceValidationFunction, config: new PoolConfig(1, acquisitionTimeout) }) - const resource1 = await pool.acquire(address) + const resource1 = await pool.acquire({}, address) expect(resource1.id).toEqual(0) expect(pool.activeResourceCount(address)).toEqual(1) @@ -735,7 +732,7 @@ describe('#unit Pool', () => { resource1.close() }, acquisitionTimeout / 2) - const resource2 = await pool.acquire(address) + const resource2 = await pool.acquire({}, address) expect(resource2.id).toEqual(1) expectNoPendingAcquisitionRequests(pool) expect(pool.activeResourceCount(address)).toEqual(1) @@ -747,18 +744,19 @@ describe('#unit Pool', () => { let counter = 0 const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, counter++, release)), destroy: res => Promise.resolve(), - validate: resourceValidOnlyOnceValidationFunction, + validateOnAcquire: (_, res) => resourceValidOnlyOnceValidationFunction(res), + validateOnRelease: resourceValidOnlyOnceValidationFunction, config: new PoolConfig(2, acquisitionTimeout) }) - const resource1 = await pool.acquire(address) + const resource1 = await pool.acquire({}, address) expect(resource1.id).toEqual(0) expect(pool.activeResourceCount(address)).toEqual(1) - const resource2 = await pool.acquire(address) + const resource2 = await pool.acquire({}, address) expect(resource2.id).toEqual(1) expect(pool.activeResourceCount(address)).toEqual(2) @@ -769,7 +767,7 @@ describe('#unit Pool', () => { resource2.close() }, acquisitionTimeout / 2) - const resource3 = await pool.acquire(address) + const resource3 = await pool.acquire({}, address) expect(resource3.id).toEqual(2) expectNoPendingAcquisitionRequests(pool) expect(pool.activeResourceCount(address)).toEqual(1) @@ -782,10 +780,9 @@ describe('#unit Pool', () => { let removeIdleObserverCount = 0 const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, resourceCount++, release)), destroy: res => Promise.resolve(), - validate: res => true, installIdleObserver: (resource, observer) => { installIdleObserverCount++ }, @@ -794,17 +791,17 @@ describe('#unit Pool', () => { } }) - const r1 = await pool.acquire(address) - const r2 = await pool.acquire(address) - const r3 = await pool.acquire(address) + const r1 = await pool.acquire({}, address) + const r2 = await pool.acquire({}, address) + const r3 = await pool.acquire({}, address) await [r1, r2, r3].map(r => r.close()) expect(installIdleObserverCount).toEqual(3) expect(removeIdleObserverCount).toEqual(0) - await pool.acquire(address) - await pool.acquire(address) - await pool.acquire(address) + await pool.acquire({}, address) + await pool.acquire({}, address) + await pool.acquire({}, address) expect(installIdleObserverCount).toEqual(3) expect(removeIdleObserverCount).toEqual(3) @@ -815,10 +812,9 @@ describe('#unit Pool', () => { let resourceCount = 0 const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, resourceCount++, release)), destroy: res => Promise.resolve(), - validate: res => true, installIdleObserver: (resource, observer) => { resource.observer = observer }, @@ -827,8 +823,8 @@ describe('#unit Pool', () => { } }) - const resource1 = await pool.acquire(address) - const resource2 = await pool.acquire(address) + const resource1 = await pool.acquire({}, address) + const resource2 = await pool.acquire({}, address) expect(pool.activeResourceCount(address)).toBe(2) await resource1.close() @@ -855,10 +851,9 @@ describe('#unit Pool', () => { let resourceCount = 0 const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, resourceCount++, release)), destroy: res => Promise.resolve(), - validate: res => true, installIdleObserver: (resource, observer) => { resource.observer = observer }, @@ -867,8 +862,8 @@ describe('#unit Pool', () => { } }) - const resource1 = await pool.acquire(address) - const resource2 = await pool.acquire(address) + const resource1 = await pool.acquire({}, address) + const resource2 = await pool.acquire({}, address) await resource1.close() await resource2.close() @@ -884,17 +879,18 @@ describe('#unit Pool', () => { let counter = 0 const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => new Promise(resolve => setTimeout( () => resolve(new Resource(server, counter++, release)) , acquisitionTimeout + 10)), destroy: res => Promise.resolve(), - validate: resourceValidOnlyOnceValidationFunction, + validateOnAcquire: (_, res) => resourceValidOnlyOnceValidationFunction(res), + validateOnRelease: resourceValidOnlyOnceValidationFunction, config: new PoolConfig(1, acquisitionTimeout) }) try { - await pool.acquire(address) + await pool.acquire({}, address) fail('should have thrown') } catch (e) { expect(e).toEqual( @@ -922,7 +918,7 @@ describe('#unit Pool', () => { }) const pool = new Pool({ - create: (server, release) => + create: (_, server, release) => Promise.resolve(new Resource(server, resourceCount++, release)), destroy: res => { resourcesReleased.push(res) @@ -933,12 +929,11 @@ describe('#unit Pool', () => { resolveRelease() } return releasePromise - }, - validate: res => true + } }) - const resource1 = await pool.acquire(address) - const resource2 = await pool.acquire(address) + const resource1 = await pool.acquire({}, address) + const resource2 = await pool.acquire({}, address) await resource1.close() await resource2.close() diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index 5ba818011..0aaae15e7 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -17,6 +17,8 @@ * limitations under the License. */ +import { object } from '../lang/index.js' + /** * Class which provides Authorization for {@link Connection} */ @@ -28,7 +30,14 @@ export default class AuthenticationProvider { this._refreshObserver = undefined } - async authenticate ({ connection }) { + async authenticate ({ connection, auth }) { + if (auth != null) { + if (connection.authToken == null || (connection.supportsReAuth && !object.equals(connection.authToken, auth))) { + return await connection.connect(this._userAgent, auth) + } + return connection + } + if (!this._authToken || this._isTokenExpired) { await this._getFreshAuthToken() } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 33a3720ea..c8524e52e 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -24,6 +24,7 @@ import { ConnectionErrorHandler } from '../connection/index.js' import { internal, error } from '../../core/index.ts' +import { object } from '../lang/index.js' const { constants: { BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, BOLT_PROTOCOL_V4_4 } @@ -42,28 +43,30 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - acquireConnection ({ accessMode, database, bookmarks } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, auth } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn, database) }) - return this._connectionPool - .acquire(this._address) - .then( - connection => - new DelegateConnection(connection, databaseSpecificErrorHandler) - ) + const connection = await this._connectionPool.acquire({ auth }, this._address) + + if (auth && !object.equals(auth, connection.authToken)) { + await connection._release() + return await this._createStickyConnection({ address: this._address, auth }) + } + + return new DelegateConnection(connection, databaseSpecificErrorHandler) } _handleAuthorizationExpired (error, address, connection, database) { this._log.warn( `Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - + this._authenticationProvider.handleError({ connection, code: error.code }) - + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { this._connectionPool.apply(address, (conn) => conn.authToken === null) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 9be46c7c8..d5b3b0ecf 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -47,7 +47,8 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._connectionPool = new Pool({ create: this._createConnection.bind(this), destroy: this._destroyConnection.bind(this), - validate: this._validateConnection.bind(this), + validateOnAcquire: this._validateConnectionOnAcquire.bind(this), + validateOnRelease: this._validateConnectionOnRelease.bind(this), installIdleObserver: PooledConnectionProvider._installIdleObserverOnConnection.bind( this ), @@ -69,13 +70,13 @@ export default class PooledConnectionProvider extends ConnectionProvider { * @return {Promise} promise resolved with a new connection or rejected when failed to connect. * @access private */ - _createConnection (address, release) { + _createConnection ({ auth }, address, release) { return this._createChannelConnection(address).then(connection => { connection._release = () => { return release(address, connection) } this._openConnections[connection.id] = connection - return this._authenticationProvider.authenticate({ connection }) + return this._authenticationProvider.authenticate({ connection, auth }) .catch(error => { // let's destroy this connection this._destroyConnection(connection) @@ -85,12 +86,32 @@ export default class PooledConnectionProvider extends ConnectionProvider { }) } + async _validateConnectionOnAcquire ({ auth }, conn) { + if (!this._validateConnection(conn)) { + return false + } + + try { + await this._authenticationProvider.authenticate({ connection: conn, auth }) + return true + } catch (error) { + this._log.info( + `The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'` + ) + return false + } + } + + _validateConnectionOnRelease (conn) { + return this._validateConnection(conn) + } + /** * Check that a connection is usable * @return {boolean} true if the connection is open * @access private **/ - async _validateConnection (conn) { + _validateConnection (conn) { if (!conn.isOpen()) { return false } @@ -101,16 +122,20 @@ export default class PooledConnectionProvider extends ConnectionProvider { return false } + return true + } + + async _createStickyConnection ({ address, auth }) { + const connection = this._createChannelConnection(address) + connection._release = () => this._destroyConnection(connection) + this._openConnections[connection.id] = connection + try { - await this._authenticationProvider.authenticate({ connection: conn }) + return await connection.connect(this._userAgent, auth) } catch (error) { - this._log.info( - `The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'` - ) - return false + await this._destroyConnection() + throw error } - - return true } /** @@ -130,7 +155,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { * @return {Promise} the server info */ async _verifyConnectivityAndGetServerVersion ({ address }) { - const connection = await this._connectionPool.acquire(address) + const connection = await this._connectionPool.acquire({}, address) const serverInfo = new ServerInfo(connection.server, connection.protocol().version) try { if (!connection.protocol().isLastMessageLogon()) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 4b9bd58ed..d5a19798b 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -28,6 +28,7 @@ import { ConnectionErrorHandler, DelegateConnection } from '../connection/index.js' +import { object } from '../lang/index.js' const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error const { @@ -186,19 +187,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider } try { - const connection = await this._acquireConnectionToServer( - address, - name, - routingTable - ) + const connection = await this._connectionPool.acquire({ auth }, address) if (auth && auth !== connection.authToken) { - if (connection.supportsReAuth) { - await connection.connect(this._userAgent, auth) - } else { - await connection._release() - return await this._createStickyConnection({ address, auth }) - } + await connection._release() + return await this._createStickyConnection({ address, auth }) } return new DelegateConnection(connection, databaseSpecificErrorHandler) @@ -316,10 +309,6 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - _acquireConnectionToServer (address, serverName, routingTable) { - return this._connectionPool.acquire(address) - } - _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { const currentRoutingTable = this._routingTableRegistry.get( database, @@ -544,33 +533,16 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - async _createStickyConnection ({ address, auth }) { - const connection = await this._createChannelConnection(address) - connection._release = () => this._destroyConnection(connection) - this._openConnections[connection.id] = connection - - try { - return await connection.connect(this._userAgent, auth) - } catch (error) { - await this._destroyConnection() - throw error - } - } - async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth) { try { - let connection = await this._connectionPool.acquire(routerAddress) + let connection = await this._connectionPool.acquire({ auth }, routerAddress) - if (auth && connection.authToken !== auth) { - if (connection.supportsReAuth) { - await await connection.connect(this._userAgent, auth) - } else { - await connection._release() - connection = await this._createStickyConnection({ - address: routerAddress, - auth - }) - } + if (auth && object.equals(auth, connection.authToken)) { + await connection._release() + connection = await this._createStickyConnection({ + address: routerAddress, + auth + }) } const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ @@ -767,7 +739,6 @@ function _isFailFastError (error) { } function _isFailFastSecurityError (error) { - console.error(error) return error.code.startsWith('Neo.ClientError.Security.') && ![ AUTHORIZATION_EXPIRED_CODE diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/lang/index.js b/packages/neo4j-driver-deno/lib/bolt-connection/lang/index.js index 2c7efd846..15b8e8340 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/lang/index.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/lang/index.js @@ -18,3 +18,4 @@ */ export * as functional from './functional.js' +export * as object from './object.js' diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/lang/object.js b/packages/neo4j-driver-deno/lib/bolt-connection/lang/object.js new file mode 100644 index 000000000..a8d7f29cb --- /dev/null +++ b/packages/neo4j-driver-deno/lib/bolt-connection/lang/object.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function equals (a, b) { + if (a === b) { + return true + } + + if (typeof a === 'object' && typeof b === 'object') { + const keysA = Object.keys(a) + const keysB = Object.keys(b) + + if (keysA.length !== keysB.length) { + return false + } + + for (const key of keysA) { + if (a[key] !== b[key]) { + return false + } + } + + return true + } + + return false +} diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js index 9519cff72..af3c002d4 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js @@ -26,15 +26,18 @@ const { class Pool { /** - * @param {function(address: ServerAddress, function(address: ServerAddress, resource: object): Promise): Promise} create + * @param {function(acquisitionContext: object, address: ServerAddress, function(address: ServerAddress, resource: object): Promise): Promise} create * an allocation function that creates a promise with a new resource. It's given an address for which to * allocate the connection and a function that will return the resource to the pool if invoked, which is * meant to be called on .dispose or .close or whatever mechanism the resource uses to finalize. + * @param {function(acquisitionContext: object, resource: object): boolean} validateOnAcquire + * called at various times when an instance is acquired + * If this returns false, the resource will be evicted + * @param {function(resource: object): boolean} validateOnRelease + * called at various times when an instance is released + * If this returns false, the resource will be evicted * @param {function(resource: object): Promise} destroy * called with the resource when it is evicted from this pool - * @param {function(resource: object): boolean} validate - * called at various times (like when an instance is acquired and when it is returned. - * If this returns false, the resource will be evicted * @param {function(resource: object, observer: { onError }): void} installIdleObserver * called when the resource is released back to pool * @param {function(resource: object): void} removeIdleObserver @@ -43,9 +46,10 @@ class Pool { * @param {Logger} log the driver logger. */ constructor ({ - create = (address, release) => Promise.resolve(), + create = (acquisitionContext, address, release) => Promise.resolve(), destroy = conn => Promise.resolve(), - validate = conn => true, + validateOnAcquire = (acquisitionContext, conn) => true, + validateOnRelease = (conn) => true, installIdleObserver = (conn, observer) => {}, removeIdleObserver = conn => {}, config = PoolConfig.defaultConfig(), @@ -53,7 +57,8 @@ class Pool { } = {}) { this._create = create this._destroy = destroy - this._validate = validate + this._validateOnAcquire = validateOnAcquire + this._validateOnRelease = validateOnRelease this._installIdleObserver = installIdleObserver this._removeIdleObserver = removeIdleObserver this._maxSize = config.maxSize @@ -69,10 +74,11 @@ class Pool { /** * Acquire and idle resource fom the pool or create a new one. + * @param {object} acquisitionContext the acquisition context used for create and validateOnAcquire connection * @param {ServerAddress} address the address for which we're acquiring. * @return {Promise} resource that is ready to use. */ - acquire (address) { + acquire (acquisitionContext, address) { const key = address.asKey() // We're out of resources and will try to acquire later on when an existing resource is released. @@ -108,7 +114,7 @@ class Pool { } }, this._acquisitionTimeout) - request = new PendingRequest(key, resolve, reject, timeoutId, this._log) + request = new PendingRequest(key, acquisitionContext, resolve, reject, timeoutId, this._log) allRequests[key].push(request) this._processPendingAcquireRequests(address) }) @@ -193,7 +199,7 @@ class Pool { return pool } - async _acquire (address) { + async _acquire (acquisitionContext, address) { if (this._closed) { throw newError('Pool is closed, it is no more able to serve requests.') } @@ -207,7 +213,7 @@ class Pool { this._removeIdleObserver(resource) } - if (await this._validate(resource)) { + if (await this._validateOnAcquire(acquisitionContext, resource)) { // idle resource is valid and can be acquired resourceAcquired(key, this._activeResourceCounts) if (this._log.isDebugEnabled()) { @@ -238,7 +244,7 @@ class Pool { let resource try { // Invoke callback that creates actual connection - resource = await this._create(address, (address, resource) => this._release(address, resource, pool)) + resource = await this._create(acquisitionContext, address, (address, resource) => this._release(address, resource, pool)) pool.pushInUse(resource) resourceAcquired(key, this._activeResourceCounts) @@ -256,7 +262,7 @@ class Pool { if (pool.isActive()) { // there exist idle connections for the given key - if (!await this._validate(resource)) { + if (!await this._validateOnRelease(resource)) { if (this._log.isDebugEnabled()) { this._log.debug( `${resource} destroyed and can't be released to the pool ${key} because it is not functional` @@ -327,7 +333,7 @@ class Pool { const pendingRequest = requests.shift() // pop a pending acquire request if (pendingRequest) { - this._acquire(address) + this._acquire(pendingRequest.context, address) .catch(error => { // failed to acquire/create a new connection to resolve the pending acquire request // propagate the error by failing the pending request @@ -391,8 +397,9 @@ function resourceReleased (key, activeResourceCounts) { } class PendingRequest { - constructor (key, resolve, reject, timeoutId, log) { + constructor (key, context, resolve, reject, timeoutId, log) { this._key = key + this._context = context this._resolve = resolve this._reject = reject this._timeoutId = timeoutId @@ -400,6 +407,10 @@ class PendingRequest { this._completed = false } + get context () { + return this._context + } + isCompleted () { return this._completed } diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index ff844f914..10ad43ec9 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -26,6 +26,7 @@ const features = [ 'Feature:API:Driver:GetServerInfo', 'Feature:API:Driver.VerifyConnectivity', 'Feature:API:Session:NotificationsConfig', + 'Optimization:AuthPipelining', 'Optimization:EagerTransactionBegin', 'Optimization:ImplicitDefaultArguments', 'Optimization:MinimalBookmarksSet', From faa5a6d94b6c781418663a0c01d1970336e2bf6a Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 16 Jan 2023 11:17:29 +0100 Subject: [PATCH 13/70] Close connection, pipeline re-auth and passing to test_renewable_auth.TestRenewableAuth5x1 tests --- .../connection-provider-direct.js | 4 +++- .../connection-provider-routing.js | 4 +++- .../src/connection/connection-channel.js | 12 +++--------- .../connection-provider-direct.js | 4 +++- .../connection-provider-routing.js | 4 +++- .../bolt-connection/connection/connection-channel.js | 12 +++--------- packages/testkit-backend/src/feature/common.js | 1 + 7 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index f5ab8c7ae..9b958e1a2 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -68,9 +68,11 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { this._authenticationProvider.handleError({ connection, code: error.code }) if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { - this._connectionPool.apply(address, (conn) => conn.authToken === null) + this._connectionPool.apply(address, (conn) => { conn.authToken = null }) } + connection.close().catch(() => undefined) + return error } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index ecbbfd8ef..b8822c95d 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -118,9 +118,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._authenticationProvider.handleError({ connection, code: error.code }) if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { - this._connectionPool.apply(address, (conn) => conn.authToken === null) + this._connectionPool.apply(address, (conn) => { conn.authToken = null }) } + connection.close().catch(() => undefined) + return error } diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 6f19478e7..e2f029605 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -196,16 +196,10 @@ export default class ChannelConnection extends Connection { throw newError('Connection does not support re-auth') } - const logoffPromise = new Promise((resolve, reject) => { - this._protocol.logoff({ onComplete: resolve, onError: reject }) - }) - - const loginPromise = new Promise((resolve, reject) => { - this._protocol.login({ onComplete: resolve, onError: reject, authToken, flush: true }) - }) + this._protocol.logoff() + this._protocol.login({ authToken }) - return await Promise.all([logoffPromise, loginPromise]) - .then(() => this) + return this } /** diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index c8524e52e..3fbecec45 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -68,9 +68,11 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { this._authenticationProvider.handleError({ connection, code: error.code }) if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { - this._connectionPool.apply(address, (conn) => conn.authToken === null) + this._connectionPool.apply(address, (conn) => { conn.authToken = null }) } + connection.close().catch(() => undefined) + return error } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index d5a19798b..9820abf28 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -118,9 +118,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._authenticationProvider.handleError({ connection, code: error.code }) if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { - this._connectionPool.apply(address, (conn) => conn.authToken === null) + this._connectionPool.apply(address, (conn) => { conn.authToken = null }) } + connection.close().catch(() => undefined) + return error } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 8ec505aa9..3b9cabe41 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -196,16 +196,10 @@ export default class ChannelConnection extends Connection { throw newError('Connection does not support re-auth') } - const logoffPromise = new Promise((resolve, reject) => { - this._protocol.logoff({ onComplete: resolve, onError: reject }) - }) - - const loginPromise = new Promise((resolve, reject) => { - this._protocol.login({ onComplete: resolve, onError: reject, authToken, flush: true }) - }) + this._protocol.logoff() + this._protocol.login({ authToken }) - return await Promise.all([logoffPromise, loginPromise]) - .then(() => this) + return this } /** diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 10ad43ec9..a3bacadbd 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -1,5 +1,6 @@ const features = [ + 'Backend:MockTime', 'Feature:Auth:Custom', 'Feature:Auth:Kerberos', 'Feature:Auth:Bearer', From 068309b7e40180fbc3d30aadf4660be1aa5faf74 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 16 Jan 2023 12:50:24 +0100 Subject: [PATCH 14/70] Ajust user switching --- .../src/connection-provider/connection-provider-pooled.js | 2 +- .../src/connection-provider/connection-provider-routing.js | 4 ++-- .../connection-provider/connection-provider-pooled.js | 2 +- .../connection-provider/connection-provider-routing.js | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 060be7b57..6f63b6154 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -133,7 +133,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { try { return await connection.connect(this._userAgent, auth) } catch (error) { - await this._destroyConnection() + await this._destroyConnection(connection) throw error } } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index b8822c95d..cbe3e6fde 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -191,7 +191,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider try { const connection = await this._connectionPool.acquire({ auth }, address) - if (auth && auth !== connection.authToken) { + if (auth && !object.equals(auth, connection.authToken)) { await connection._release() return await this._createStickyConnection({ address, auth }) } @@ -539,7 +539,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider try { let connection = await this._connectionPool.acquire({ auth }, routerAddress) - if (auth && object.equals(auth, connection.authToken)) { + if (auth && !object.equals(auth, connection.authToken)) { await connection._release() connection = await this._createStickyConnection({ address: routerAddress, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index d5b3b0ecf..94cc22175 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -133,7 +133,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { try { return await connection.connect(this._userAgent, auth) } catch (error) { - await this._destroyConnection() + await this._destroyConnection(connection) throw error } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 9820abf28..00cc7af13 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -191,7 +191,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider try { const connection = await this._connectionPool.acquire({ auth }, address) - if (auth && auth !== connection.authToken) { + if (auth && !object.equals(auth, connection.authToken)) { await connection._release() return await this._createStickyConnection({ address, auth }) } @@ -539,7 +539,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider try { let connection = await this._connectionPool.acquire({ auth }, routerAddress) - if (auth && object.equals(auth, connection.authToken)) { + if (auth && !object.equals(auth, connection.authToken)) { await connection._release() connection = await this._createStickyConnection({ address: routerAddress, From 6d0c2966aeb7c3e2a3763cf2aa194ff39c29e6aa Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 17 Jan 2023 11:01:17 +0100 Subject: [PATCH 15/70] Add tests to DenoJS --- .../connection-provider-pooled.js | 2 +- .../connection-provider-pooled.js | 2 +- packages/testkit-backend/deno/controller.ts | 3 ++- packages/testkit-backend/deno/deps.ts | 1 + packages/testkit-backend/deno/domain.ts | 7 ++++++- packages/testkit-backend/src/controller/local.js | 8 +++++++- packages/testkit-backend/src/mock/fake-time.js | 15 +++++++++++++++ .../testkit-backend/src/request-handlers-rx.js | 2 +- packages/testkit-backend/src/request-handlers.js | 10 +++++++--- 9 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 packages/testkit-backend/src/mock/fake-time.js diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 6f63b6154..7ea71079a 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -126,7 +126,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { } async _createStickyConnection ({ address, auth }) { - const connection = this._createChannelConnection(address) + const connection = await this._createChannelConnection(address) connection._release = () => this._destroyConnection(connection) this._openConnections[connection.id] = connection diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 94cc22175..2990eb5c7 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -126,7 +126,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { } async _createStickyConnection ({ address, auth }) { - const connection = this._createChannelConnection(address) + const connection = await this._createChannelConnection(address) connection._release = () => this._destroyConnection(connection) this._openConnections[connection.id] = connection diff --git a/packages/testkit-backend/deno/controller.ts b/packages/testkit-backend/deno/controller.ts index eafd454c5..3fb58bc76 100644 --- a/packages/testkit-backend/deno/controller.ts +++ b/packages/testkit-backend/deno/controller.ts @@ -1,4 +1,5 @@ import Context from "../src/context.js"; +import { FakeTime } from "./deps.ts"; import { RequestHandlerMap, TestkitRequest, @@ -74,7 +75,7 @@ export function createHandler( const handleRequest = requestHandlers[name]; - handleRequest(neo4j, context, data, wire); + handleRequest({ neo4j, mock: { FakeTime } }, context, data, wire); } }; } diff --git a/packages/testkit-backend/deno/deps.ts b/packages/testkit-backend/deno/deps.ts index 898990acd..bee7f42fa 100644 --- a/packages/testkit-backend/deno/deps.ts +++ b/packages/testkit-backend/deno/deps.ts @@ -1,4 +1,5 @@ export { iterateReader } from "https://deno.land/std@0.119.0/streams/conversion.ts"; +export { FakeTime } from "https://deno.land/std@0.165.0/testing/time.ts"; export { default as Context } from "../src/context.js"; export { getShouldRunTest } from "../src/skipped-tests/index.js"; export { default as neo4j } from "../../neo4j-driver-deno/lib/mod.ts"; diff --git a/packages/testkit-backend/deno/domain.ts b/packages/testkit-backend/deno/domain.ts index 74d5e817b..41b02fc86 100644 --- a/packages/testkit-backend/deno/domain.ts +++ b/packages/testkit-backend/deno/domain.ts @@ -1,5 +1,6 @@ // deno-lint-ignore-file no-explicit-any import Context from "../src/context.js"; +import { FakeTime } from "./deps.ts"; export interface TestkitRequest { name: string; @@ -11,8 +12,12 @@ export interface TestkitResponse { data?: any; } +export interface Mock { + FakeTime: typeof FakeTime; +} + export interface RequestHandler { - (neo4j: any, c: Context, data: any, wire: any): void; + (service: { neo4j: any; mock: Mock }, c: Context, data: any, wire: any): void; } export interface RequestHandlerMap { diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js index 2d5c80af0..0d8f190ad 100644 --- a/packages/testkit-backend/src/controller/local.js +++ b/packages/testkit-backend/src/controller/local.js @@ -3,6 +3,7 @@ import Controller from './interface' import stringify from '../stringify' import { isFrontendError } from '../request-handlers' import CypherNativeBinders from '../cypher-native-binders' +import FakeTime from '../mock/fake-time' /** * Local controller handles the requests locally by redirecting them to the correct request handler/service. @@ -37,7 +38,12 @@ export default class LocalController extends Controller { throw new Error(`Unknown request: ${name}`) } - return await this._requestHandlers[name](this._neo4j, this._contexts.get(contextId), data, { + return await this._requestHandlers[name]({ + neo4j: this._neo4j, + mock: { + FakeTime + } + }, this._contexts.get(contextId), data, { writeResponse: (response) => this._writeResponse(contextId, response), writeError: (e) => this._writeError(contextId, e), writeBackendError: (msg) => this._writeBackendError(contextId, msg) diff --git a/packages/testkit-backend/src/mock/fake-time.js b/packages/testkit-backend/src/mock/fake-time.js new file mode 100644 index 000000000..53f38546a --- /dev/null +++ b/packages/testkit-backend/src/mock/fake-time.js @@ -0,0 +1,15 @@ +import sinon from 'sinon' + +export default class FakeTime { + constructor (time) { + this._clock = sinon.useFakeTimers(time || new Date().getTime()) + } + + tick (incrementMs) { + this._clock.tick(incrementMs) + } + + restore () { + this._clock.restore() + } +} diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 6e1879e10..00e78690f 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -32,7 +32,7 @@ export { FakeTimeUninstall } from './request-handlers.js' -export function NewSession (neo4j, context, data, wire) { +export function NewSession ({ neo4j }, context, data, wire) { let { driverId, accessMode, bookmarks, database, fetchSize, impersonatedUser, bookmarkManagerId } = data switch (accessMode) { case 'r': diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 5103245dc..110f6decc 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -1,5 +1,4 @@ import * as responses from './responses.js' -import sinon from 'sinon' export function throwFrontendError () { throw new Error('TestKit FrontendError') @@ -9,7 +8,7 @@ export function isFrontendError (error) { return error.message === 'TestKit FrontendError' } -export function NewDriver (neo4j, context, data, wire) { +export function NewDriver ({ neo4j }, context, data, wire) { const { uri, authorizationToken, @@ -101,7 +100,7 @@ export function DriverClose (_, context, data, wire) { .catch(err => wire.writeError(err)) } -export function NewSession (neo4j, context, data, wire) { +export function NewSession ({ neo4j }, context, data, wire) { let { driverId, accessMode, bookmarks, database, fetchSize, impersonatedUser, bookmarkManagerId } = data switch (accessMode) { case 'r': @@ -629,6 +628,10 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config export function FakeTimeInstall (_, context, _data, wire) { context.clock = sinon.useFakeTimers(new Date().getTime()) +} + +export function FakeTimeInstall ({ mock }, context, _data, wire) { + context.clock = new mock.FakeTime() wire.writeResponse(responses.FakeTimeAck()) } @@ -639,5 +642,6 @@ export function FakeTimeTick (_, context, { incrementMs }, wire) { export function FakeTimeUninstall (_, context, _data, wire) { context.clock.restore() + delete context.clock wire.writeResponse(responses.FakeTimeAck()) } From cbce3c854964d2a3806329eb97f7e11e38931e82 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 17 Jan 2023 11:56:31 +0100 Subject: [PATCH 16/70] Testing pool changes --- .../bolt-connection/test/pool/pool.test.js | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/packages/bolt-connection/test/pool/pool.test.js b/packages/bolt-connection/test/pool/pool.test.js index 48d20c0eb..2aa80c644 100644 --- a/packages/bolt-connection/test/pool/pool.test.js +++ b/packages/bolt-connection/test/pool/pool.test.js @@ -125,6 +125,126 @@ describe('#unit Pool', () => { expect(destroyed[1].id).toBe(r1.id) }) + it('frees if validateOnRelease returns Promise.resolve(false)', async () => { + // Given a pool that allocates + let counter = 0 + const destroyed = [] + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_acquisitionContext, server, release) => + Promise.resolve(new Resource(server, counter++, release)), + destroy: res => { + destroyed.push(res) + return Promise.resolve() + }, + validateOnRelease: res => Promise.resolve(false), + config: new PoolConfig(1000, 60000) + }) + + // When + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) + + // Then + await r0.close() + await r1.close() + + expect(destroyed.length).toBe(2) + expect(destroyed[0].id).toBe(r0.id) + expect(destroyed[1].id).toBe(r1.id) + }) + + it('does not free if validateOnRelease returns Promise.resolve(true)', async () => { + // Given a pool that allocates + let counter = 0 + const destroyed = [] + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_acquisitionContext, server, release) => + Promise.resolve(new Resource(server, counter++, release)), + destroy: res => { + destroyed.push(res) + return Promise.resolve() + }, + validateOnRelease: res => Promise.resolve(true), + config: new PoolConfig(1000, 60000) + }) + + // When + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) + + // Then + await r0.close() + await r1.close() + + expect(destroyed.length).toBe(0) + }) + + it('frees if validateOnAcquire returns Promise.resolve(false)', async () => { + // Given a pool that allocates + let counter = 0 + const destroyed = [] + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_acquisitionContext, server, release) => + Promise.resolve(new Resource(server, counter++, release)), + destroy: res => { + destroyed.push(res) + return Promise.resolve() + }, + validateOnAcquire: res => Promise.resolve(false), + config: new PoolConfig(1000, 60000) + }) + + // When + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) + await r1.close() + await r0.close() + + // Then + const r2 = await pool.acquire({}, address) + + // Closing + await r2.close() + + expect(destroyed.length).toBe(2) + expect(destroyed[0].id).toBe(r0.id) + expect(destroyed[1].id).toBe(r1.id) + }) + + it('does not free if validateOnAcquire returns Promise.resolve(true)', async () => { + // Given a pool that allocates + let counter = 0 + const destroyed = [] + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_acquisitionContext, server, release) => + Promise.resolve(new Resource(server, counter++, release)), + destroy: res => { + destroyed.push(res) + return Promise.resolve() + }, + validateOnAcquire: res => Promise.resolve(true), + config: new PoolConfig(1000, 60000) + }) + + // When + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address) + await r0.close() + await r1.close() + + // Then + const r2 = await pool.acquire({}, address) + + // Closing + await r2.close() + + expect(destroyed.length).toBe(0) + }) + it('purges keys', async () => { // Given a pool that allocates let counter = 0 From fb05d796eb632dd0c1cbfaa55527276618b6b1eb Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 17 Jan 2023 12:40:24 +0100 Subject: [PATCH 17/70] Add 5.1 base protocol test and supportLogoff test --- .../connection-provider-direct.test.js | 8 ++++++++ .../connection-provider-routing.test.js | 8 ++++++++ packages/neo4j-driver/test/internal/fake-connection.js | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index d171fc89b..f04ecbbb7 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -351,6 +351,10 @@ class FakeConnection extends Connection { return this._authToken } + set authToken (authToken) { + this._authToken = this.authToken + } + get address () { return this._address } @@ -358,4 +362,8 @@ class FakeConnection extends Connection { get server () { return this._server } + + async close () { + + } } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 5626cda28..7dcea43bd 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3109,6 +3109,10 @@ class FakeConnection extends Connection { return this._authToken } + set authToken (authToken) { + this._authToken = authToken + } + get address () { return this._address } @@ -3121,6 +3125,10 @@ class FakeConnection extends Connection { return this._server } + async close () { + + } + protocol () { return { version: this._protocolVersion, diff --git a/packages/neo4j-driver/test/internal/fake-connection.js b/packages/neo4j-driver/test/internal/fake-connection.js index 415f04f03..75b9497a8 100644 --- a/packages/neo4j-driver/test/internal/fake-connection.js +++ b/packages/neo4j-driver/test/internal/fake-connection.js @@ -83,6 +83,10 @@ export default class FakeConnection extends Connection { return this._authToken } + set authToken (authToken) { + this._authToken = authToken + } + protocol () { // return fake protocol object that simply records seen queries and parameters return { @@ -179,4 +183,8 @@ export default class FakeConnection extends Connection { this._open = false return this } + + async close () { + this._open = false + } } From f640d8d573c1bb2157ab944a873e1442af536052 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 17 Jan 2023 14:29:52 +0100 Subject: [PATCH 18/70] Add test for login/logoff in a non supported protocol --- packages/bolt-connection/src/bolt/bolt-protocol-v1.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index 35c8b5dfa..1ef3c169a 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -205,6 +205,7 @@ export default class BoltProtocol { onError: onError }) + // TODO: Verify the Neo4j version in the message const error = newError( 'Driver is connected to a database that does not support logoff. ' + 'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.' @@ -233,8 +234,9 @@ export default class BoltProtocol { onError: (error) => this._onLoginError(error, onError) }) + // TODO: Verify the Neo4j version in the message const error = newError( - 'Driver is connected to a database that does not support logon. ' + + 'Driver is connected to a database that does not support login. ' + 'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.' ) From cae34c9a32fcd90a7bb1051d01970db2a919b329 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 17 Jan 2023 14:51:19 +0100 Subject: [PATCH 19/70] Add test to the connection-channel --- .../src/connection/connection-channel.js | 9 +-- .../connection/connection-channel.test.js | 68 +++++++++++++++++++ .../bolt-connection/bolt/bolt-protocol-v1.js | 2 + .../connection/connection-channel.js | 9 +-- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index e2f029605..dda84cfe9 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -187,15 +187,16 @@ export default class ChannelConnection extends Connection { * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ async connect (userAgent, authToken) { + if (this._protocol.initialized && !this._protocol.supportsLogoff) { + throw newError('Connection does not support re-auth') + } + this._authToken = authToken + if (!this._protocol.initialized) { return await this._initialize(userAgent, authToken) } - if (!this._protocol.supportsLogoff) { - throw newError('Connection does not support re-auth') - } - this._protocol.logoff() this._protocol.login({ authToken }) diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 18200eea5..196604a80 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -149,6 +149,74 @@ describe('ChannelConnection', () => { expect(call.notificationFilter).toBe(notificationFilter) } ) + it('should set the AuthToken in the context', async () => { + const authToken = { + scheme: 'none' + } + const protocol = { + initialize: jest.fn(observer => observer.onComplete({})) + } + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier }) + + await connection.connect('userAgent', authToken) + + expect(connection.authToken).toEqual(authToken) + }) + + describe('re-auth', () => { + describe('when protocol support re-auth', () => { + it('should call logoff and login', async () => { + const authToken = { + scheme: 'none' + } + const protocol = { + initialize: jest.fn(observer => observer.onComplete({})), + logoff: jest.fn(() => undefined), + login: jest.fn(() => undefined), + initialized: true, + supportsLogoff: true + } + + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier }) + + await connection.connect('userAgent', authToken) + + expect(protocol.initialize).not.toHaveBeenCalled() + expect(protocol.logoff).toHaveBeenCalledWith() + expect(protocol.login).toHaveBeenCalledWith({ authToken }) + expect(connection.authToken).toEqual(authToken) + }) + }) + + describe('when protocol does not support re-auth', () => { + it('should throw connection does not support re-auth', async () => { + const authToken = { + scheme: 'none' + } + const protocol = { + initialize: jest.fn(observer => observer.onComplete({})), + logoff: jest.fn(() => undefined), + login: jest.fn(() => undefined), + initialized: true, + supportsLogoff: false + } + + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier }) + + await expect(connection.connect('userAgent', authToken)).rejects.toThrow( + newError('Connection does not support re-auth') + ) + + expect(protocol.initialize).not.toHaveBeenCalled() + expect(protocol.logoff).not.toHaveBeenCalled() + expect(protocol.login).not.toHaveBeenCalled() + expect(connection.authToken).toEqual(null) + }) + }) + }) }) describe('._handleFatalError()', () => { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js index bafc8d74e..1212bb019 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -205,6 +205,7 @@ export default class BoltProtocol { onError: onError }) + // TODO: Verify the Neo4j version in the message const error = newError( 'Driver is connected to a database that does not support logoff. ' + 'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.' @@ -233,6 +234,7 @@ export default class BoltProtocol { onError: (error) => this._onLoginError(error, onError) }) + // TODO: Verify the Neo4j version in the message const error = newError( 'Driver is connected to a database that does not support logon. ' + 'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.' diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 3b9cabe41..3cc39b712 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -187,15 +187,16 @@ export default class ChannelConnection extends Connection { * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ async connect (userAgent, authToken) { + if (this._protocol.initialized && !this._protocol.supportsLogoff) { + throw newError('Connection does not support re-auth') + } + this._authToken = authToken + if (!this._protocol.initialized) { return await this._initialize(userAgent, authToken) } - if (!this._protocol.supportsLogoff) { - throw newError('Connection does not support re-auth') - } - this._protocol.logoff() this._protocol.login({ authToken }) From 1b6950ed6373de13eba6e8432cfb91289a0eeb1b Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 17 Jan 2023 16:48:21 +0100 Subject: [PATCH 20/70] Add tests for AuthenticationProvider#authenticate, no user-switching --- .../authentication-provider.js | 2 +- .../authorization-provider.test.js | 425 ++++++++++++++++++ .../authentication-provider.js | 2 +- 3 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 packages/bolt-connection/test/connection-provider/authorization-provider.test.js diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index d35d93991..02f525335 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -42,7 +42,7 @@ export default class AuthenticationProvider { await this._getFreshAuthToken() } - if (this._renewableAuthToken.authToken !== connection.authToken) { + if (!object.equals(this._renewableAuthToken.authToken, connection.authToken)) { return await connection.connect(this._userAgent, this._authToken) } diff --git a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js new file mode 100644 index 000000000..20a44e606 --- /dev/null +++ b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js @@ -0,0 +1,425 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import AuthenticationProvider from '../../src/connection-provider/authentication-provider' + +describe('AuthenticationProvider', () => { + const USER_AGENT = 'javascript-driver/5.5.0' + + describe('.authenticate()', () => { + describe('when called without an auth', () => { + describe('and first call', () => { + describe('and connection.authToken is different of new AuthToken', () => { + it('should refresh the auth token', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(authTokenProvider).toHaveBeenCalledTimes(1) + }) + + it('should refresh authToken only once', async () => { + const authTokenProvider = jest.fn(() => new Promise((resolve) => { + setTimeout(() => { + resolve(toRenewableToken({ scheme: 'none' })) + }, 100) + })) + + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connections = [mockConnection(), mockConnection()] + + await Promise.all(connections.map(connection => authenticationProvider.authenticate({ connection }))) + + expect(authTokenProvider).toHaveBeenCalledTimes(1) + }) + + it('should return the connection', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection() + + const resultedConnection = await authenticationProvider.authenticate({ connection }) + + expect(resultedConnection).toBe(connection) + }) + + it('should call connection.connect', async () => { + const authToken = { scheme: 'none' } + const authTokenProvider = jest.fn(() => toRenewableToken(authToken)) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, authToken) + }) + + it('should throw errors happened during token refresh', async () => { + const error = new Error('ops') + const authTokenProvider = jest.fn(() => Promise.reject(error)) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection() + + await expect(authenticationProvider.authenticate({ connection })).rejects.toThrow(error) + }) + + it('should throw errors happened during connection.connect', async () => { + const error = new Error('ops') + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ + connect: () => Promise.reject(error) + }) + + await expect(authenticationProvider.authenticate({ connection })).rejects.toThrow(error) + }) + }) + + describe('when connection.authToken is equal to new AuthToken', () => { + it('should refresh the auth token', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(authTokenProvider).toHaveBeenCalledTimes(1) + }) + + it('should refresh authToken only once', async () => { + const authTokenProvider = jest.fn(() => new Promise((resolve) => { + setTimeout(() => { + resolve(toRenewableToken({ scheme: 'none' })) + }, 100) + })) + + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connections = [mockConnection(), mockConnection()] + + await Promise.all(connections.map(connection => authenticationProvider.authenticate({ connection }))) + + expect(authTokenProvider).toHaveBeenCalledTimes(1) + }) + + it('should return the connection', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection() + + const resultedConnection = await authenticationProvider.authenticate({ connection }) + + expect(resultedConnection).toBe(connection) + }) + + it('should not call connection.connect', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ authToken: { scheme: 'none' } }) + + await authenticationProvider.authenticate({ connection }) + + expect(connection.connect).toHaveBeenCalledTimes(0) + }) + + it('should throw errors happened during token refresh', async () => { + const error = new Error('ops') + const authTokenProvider = jest.fn(() => Promise.reject(error)) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection() + + await expect(authenticationProvider.authenticate({ connection })).rejects.toThrow(error) + }) + }) + }) + + describe('and token has expired', () => { + describe('and connection.authToken is different of new AuthToken', () => { + it('should refresh the auth token', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(authTokenProvider).toHaveBeenCalledTimes(1) + }) + + it('should refresh authToken only once', async () => { + const authTokenProvider = jest.fn(() => new Promise((resolve) => { + setTimeout(() => { + resolve(toRenewableToken({ scheme: 'none' })) + }, 100) + })) + + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connections = [mockConnection(), mockConnection()] + + await Promise.all(connections.map(connection => authenticationProvider.authenticate({ connection }))) + + expect(authTokenProvider).toHaveBeenCalledTimes(1) + }) + + it('should return the connection', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + const resultedConnection = await authenticationProvider.authenticate({ connection }) + + expect(resultedConnection).toBe(connection) + }) + + it('should call connection.connect', async () => { + const authToken = { scheme: 'none' } + const authTokenProvider = jest.fn(() => toRenewableToken(authToken)) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, authToken) + }) + + it('should throw errors happened during token refresh', async () => { + const error = new Error('ops') + const authTokenProvider = jest.fn(() => Promise.reject(error)) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + await expect(authenticationProvider.authenticate({ connection })).rejects.toThrow(error) + }) + + it('should throw errors happened during connection.connect', async () => { + const error = new Error('ops') + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection({ + connect: () => Promise.reject(error) + }) + + await expect(authenticationProvider.authenticate({ connection })).rejects.toThrow(error) + }) + }) + + describe('when connection.authToken is equal to new AuthToken', () => { + it('should refresh the auth token', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(authTokenProvider).toHaveBeenCalledTimes(1) + }) + + it('should refresh authToken only once', async () => { + const authTokenProvider = jest.fn(() => new Promise((resolve) => { + setTimeout(() => { + resolve(toRenewableToken({ scheme: 'none' })) + }, 100) + })) + + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connections = [mockConnection(), mockConnection()] + + await Promise.all(connections.map(connection => authenticationProvider.authenticate({ connection }))) + + expect(authTokenProvider).toHaveBeenCalledTimes(1) + }) + + it('should return the connection', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + const resultedConnection = await authenticationProvider.authenticate({ connection }) + + expect(resultedConnection).toBe(connection) + }) + + it('should not call connection.connect', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({ scheme: 'none' })) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection({ authToken: { scheme: 'none' } }) + + await authenticationProvider.authenticate({ connection }) + + expect(connection.connect).toHaveBeenCalledTimes(0) + }) + + it('should throw errors happened during token refresh', async () => { + const error = new Error('ops') + const authTokenProvider = jest.fn(() => Promise.reject(error)) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + await expect(authenticationProvider.authenticate({ connection })).rejects.toThrow(error) + }) + }) + }) + + describe('and token is not expired', () => { + describe('and connection.authToken is different of provider.authToken', () => { + it('should not refresh the auth token', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(authTokenProvider).toHaveBeenCalledTimes(0) + }) + + it('should return the connection', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + const resultedConnection = await authenticationProvider.authenticate({ connection }) + + expect(resultedConnection).toBe(connection) + }) + + it('should call connection.connect', async () => { + const authToken = { scheme: 'none' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, authToken) + }) + + it('should throw errors happened during connection.connect', async () => { + const error = new Error('ops') + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection({ + connect: () => Promise.reject(error) + }) + + await expect(authenticationProvider.authenticate({ connection })).rejects.toThrow(error) + }) + }) + + describe('when connection.authToken is equal to provider.authToken', () => { + it('should not refresh the auth token', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + await authenticationProvider.authenticate({ connection }) + + expect(authTokenProvider).toHaveBeenCalledTimes(0) + }) + + it('should return the connection', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection() + + const resultedConnection = await authenticationProvider.authenticate({ connection }) + + expect(resultedConnection).toBe(connection) + }) + + it('should not call connection.connect', async () => { + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken({ scheme: 'none' }) + }) + const connection = mockConnection({ authToken: { scheme: 'none' } }) + + await authenticationProvider.authenticate({ connection }) + + expect(connection.connect).toHaveBeenCalledTimes(0) + }) + }) + }) + }) + }) + + function createAuthenticationProvider (authTokenProvider, mocks) { + const provider = new AuthenticationProvider({ + authTokenProvider, + userAgent: USER_AGENT + }) + + if (mocks) { + provider._renewableAuthToken = mocks.renewableAuthToken + } + + return provider + } + + function mockConnection ({ connect, authToken, supportsReAuth } = {}) { + const connection = { + connect: connect || jest.fn(() => Promise.resolve(connection)), + authToken, + supportsReAuth + } + return connection + } + + function toRenewableToken (authToken, expectedExpirationTime) { + return { + authToken, + expectedExpirationTime + } + } + + function toExpiredRenewableToken (authToken) { + return { + authToken, + expectedExpirationTime: new Date(new Date().getTime() - 1) + } + } +}) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index 0aaae15e7..20f052711 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -42,7 +42,7 @@ export default class AuthenticationProvider { await this._getFreshAuthToken() } - if (this._renewableAuthToken.authToken !== connection.authToken) { + if (!object.equals(this._renewableAuthToken.authToken, connection.authToken)) { return await connection.connect(this._userAgent, this._authToken) } From 49f34ac744382536d9d1d757d80148100a829090 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 18 Jan 2023 18:26:12 +0100 Subject: [PATCH 21/70] Add AuthenticationProvider.authenticate when auth is provider --- .../authorization-provider.test.js | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js index 20a44e606..188a437e9 100644 --- a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js @@ -385,6 +385,175 @@ describe('AuthenticationProvider', () => { }) }) }) + + describe.each([ + ['and first call', createAuthenticationProvider], + ['and token has expired', (authTokenProvider) => createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none', credentials: 'token expired' }) + })], + ['and toke is not expired', (authTokenProvider) => createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toExpiredRenewableToken({ scheme: 'none' }) + })] + ])('when called with an auth and %s', (_, createAuthenticationProvider) => { + describe.each([false, true])('and connection is not authenticated (supportsReAuth=%s)', (supportsReAuth) => { + it('should call connection connect with the supplied auth', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth }) + + await authenticationProvider.authenticate({ connection, auth }) + + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, auth) + }) + + it('should return the connection', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth }) + + await expect(authenticationProvider.authenticate({ connection, auth })).resolves.toBe(connection) + }) + + it('should not refresh the token', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth }) + + await authenticationProvider.authenticate({ connection, auth }) + + expect(authTokenProvider).not.toHaveBeenCalled() + }) + + it('should throws if connection fails', async () => { + const error = new Error('nope') + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ + supportsReAuth, + connect: jest.fn(() => Promise.reject(error)) + }) + + await expect(authenticationProvider.authenticate({ connection, auth })).rejects.toThrow(error) + }) + }) + + describe.each([false, true])('and connection is authenticated with same token (supportsReAuth=%s)', (supportsReAuth) => { + it('should not call connection connect with the supplied auth', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth } }) + + await authenticationProvider.authenticate({ connection, auth }) + + expect(connection.connect).not.toHaveBeenCalledWith(USER_AGENT, auth) + }) + + it('should return the connection', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth } }) + + await expect(authenticationProvider.authenticate({ connection, auth })).resolves.toBe(connection) + }) + + it('should not refresh the token', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth } }) + + await authenticationProvider.authenticate({ connection, auth }) + + expect(authTokenProvider).not.toHaveBeenCalled() + }) + }) + + describe.each([true])('and connection is authenticated with different token (supportsReAuth=%s)', (supportsReAuth) => { + it('should call connection connect with the supplied auth', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { scheme: 'bearer', credentials: 'other' } }) + + await authenticationProvider.authenticate({ connection, auth }) + + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, auth) + }) + + it('should return the connection', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { scheme: 'bearer', credentials: 'other' } }) + + await expect(authenticationProvider.authenticate({ connection, auth })).resolves.toBe(connection) + }) + + it('should not refresh the token', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { scheme: 'bearer', credentials: 'other' } }) + + await authenticationProvider.authenticate({ connection, auth }) + + expect(authTokenProvider).not.toHaveBeenCalled() + }) + + it('should throws if connection fails', async () => { + const error = new Error('nope') + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ + supportsReAuth, + connect: jest.fn(() => Promise.reject(error)), + authToken: { scheme: 'bearer', credentials: 'other' } + }) + + await expect(authenticationProvider.authenticate({ connection, auth })).rejects.toThrow(error) + }) + }) + + describe.each([false])('and connection is authenticated with different token (supportsReAuth=%s)', (supportsReAuth) => { + it('should not call connection connect with the supplied auth', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth, credentials: 'other' } }) + + await authenticationProvider.authenticate({ connection, auth }) + + expect(connection.connect).not.toHaveBeenCalledWith(USER_AGENT, auth) + }) + + it('should return the connection', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth, credentials: 'other' } }) + + await expect(authenticationProvider.authenticate({ connection, auth })).resolves.toBe(connection) + }) + + it('should not refresh the token', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth, credentials: 'other' } }) + + await authenticationProvider.authenticate({ connection, auth }) + + expect(authTokenProvider).not.toHaveBeenCalled() + }) + }) + }) }) function createAuthenticationProvider (authTokenProvider, mocks) { From a9bf70434d1f3229f25dde17e46f0dd9e002cc6c Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 18 Jan 2023 19:25:21 +0100 Subject: [PATCH 22/70] Test AuthenticationProvider.handleError --- .../authentication-provider.js | 6 +- .../authorization-provider.test.js | 171 ++++++++++++++++++ .../authentication-provider.js | 6 +- 3 files changed, 177 insertions(+), 6 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index 02f525335..12904665c 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -49,9 +49,9 @@ export default class AuthenticationProvider { return connection } - async handleError ({ connection, code }) { - if ( - connection.authToken === this._authToken && + handleError ({ connection, code }) { + if ( + object.equals(connection.authToken, this._authToken) && [ 'Neo.ClientError.Security.Unauthorized', 'Neo.ClientError.Security.TokenExpired' diff --git a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js index 188a437e9..2a2e03eb5 100644 --- a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js @@ -556,6 +556,159 @@ describe('AuthenticationProvider', () => { }) }) + describe('.handleError()', () => { + it.each( + shouldNotScheduleRefreshScenarios() + )('should not schedule a refresh when %s', (_, createScenario) => { + const { + connection, + code, + authTokenProvider, + authenticationProvider + } = createScenario() + + authenticationProvider.handleError({ code, connection }) + + expect(authTokenProvider).not.toHaveBeenCalled() + }) + + it.each( + errorCodeTriggerRefreshAuth() + )('should schedule refresh when auth are the same, valid error code (%s) and no refresh schedule', async (code) => { + const authToken = { scheme: 'bearer', credentials: 'token' } + const newTokenPromiseState = {} + const newTokenPromise = new Promise((resolve) => { newTokenPromiseState.resolve = resolve }) + const authTokenProvider = jest.fn(() => newTokenPromise) + const renewableAuthToken = toRenewableToken(authToken) + const connection = mockConnection({ + authToken: { ...authToken } + }) + const authenticationProvider = createAuthenticationProvider(authTokenProvider, { + renewableAuthToken + }) + + authenticationProvider.handleError({ code, connection }) + + expect(authTokenProvider).toHaveBeenCalled() + + // Test implementation details + expect(authenticationProvider._renewableAuthToken).toEqual(renewableAuthToken) + + const newRenewableToken = toRenewableToken({ scheme: 'bearer', credentials: 'token2' }) + newTokenPromiseState.resolve(newRenewableToken) + + await newTokenPromise + + expect(authenticationProvider._renewableAuthToken).toBe(newRenewableToken) + }) + + function shouldNotScheduleRefreshScenarios () { + return [ + ...nonValidCodesScenarios(), + ...validCodesWithDifferentAuthScenarios(), + ...nonValidCodesWithDifferentAuthScenarios(), + ...validCodesWithSameAuthButWithRescheduleInPlaceScenarios() + ] + + function nonValidCodesScenarios () { + return [ + 'Neo.ClientError.Security.AuthorizationExpired', + 'Neo.ClientError.General.ForbiddenOnReadOnlyDatabase', + 'Neo.Made.Up.Error' + ].flatMap(code => [ + [ + `connection and provider has same auth token and error code does not trigger re-fresh (code=${code})`, () => { + const authToken = { scheme: 'bearer', credentials: 'token' } + const authTokenProvider = jest.fn(() => {}) + return { + connection: mockConnection({ + authToken: { ...authToken } + }), + code, + authTokenProvider, + authenticationProvider: createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken(authToken) + }) + } + } + ] + ]) + } + + function validCodesWithDifferentAuthScenarios () { + return errorCodeTriggerRefreshAuth().flatMap(code => [ + [ + `connection and provider has different auth token and error code does trigger re-fresh (code=${code})`, + () => { + const authToken = { scheme: 'bearer', credentials: 'token' } + const authTokenProvider = jest.fn(() => {}) + return { + connection: mockConnection({ + authToken: { ...authToken, credentials: 'token2' } + }), + code, + authTokenProvider, + authenticationProvider: createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken(authToken) + }) + } + } + ] + + ]) + } + + function nonValidCodesWithDifferentAuthScenarios () { + return [ + 'Neo.ClientError.Security.AuthorizationExpired', + 'Neo.ClientError.General.ForbiddenOnReadOnlyDatabase', + 'Neo.Made.Up.Error' + ].flatMap(code => [ + [ + `connection and provider has different auth token and error code does not trigger re-fresh (code=${code})`, + () => { + const authToken = { scheme: 'bearer', credentials: 'token' } + const authTokenProvider = jest.fn(() => {}) + return { + connection: mockConnection({ + authToken: { ...authToken, credentials: 'token2' } + }), + code, + authTokenProvider, + authenticationProvider: createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken(authToken) + }) + } + } + ] + + ]) + } + + function validCodesWithSameAuthButWithRescheduleInPlaceScenarios () { + return errorCodeTriggerRefreshAuth().flatMap(code => [ + [ + `connection and provider has same auth token and error code does trigger re-fresh (code=${code}), but refresh already schedule`, () => { + const authToken = { scheme: 'bearer', credentials: 'token' } + const authTokenProvider = jest.fn(() => {}) + return { + connection: mockConnection({ + authToken: { ...authToken } + }), + code, + authTokenProvider, + authenticationProvider: createAuthenticationProvider(authTokenProvider, { + renewableAuthToken: toRenewableToken(authToken), + refreshObserver: refreshObserverMock() + }) + } + } + ] + ]) + } + } + }) + function createAuthenticationProvider (authTokenProvider, mocks) { const provider = new AuthenticationProvider({ authTokenProvider, @@ -564,6 +717,7 @@ describe('AuthenticationProvider', () => { if (mocks) { provider._renewableAuthToken = mocks.renewableAuthToken + provider._refreshObserver = mocks.refreshObserver } return provider @@ -591,4 +745,21 @@ describe('AuthenticationProvider', () => { expectedExpirationTime: new Date(new Date().getTime() - 1) } } + + function errorCodeTriggerRefreshAuth () { + return [ + 'Neo.ClientError.Security.Unauthorized', + 'Neo.ClientError.Security.TokenExpired' + ] + } + + function refreshObserverMock () { + const subscribers = [] + + return { + subscribe: (sub) => subscribers.push(sub), + notify: () => subscribers.forEach(sub => sub.onSuccess()), + notifyError: (e) => subscribers.forEach(sub => sub.onError(e)) + } + } }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index 20f052711..fc380ebc6 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -49,9 +49,9 @@ export default class AuthenticationProvider { return connection } - async handleError ({ connection, code }) { - if ( - connection.authToken === this._authToken && + handleError ({ connection, code }) { + if ( + object.equals(connection.authToken, this._authToken) && [ 'Neo.ClientError.Security.Unauthorized', 'Neo.ClientError.Security.TokenExpired' From e0ad1a0e8cd49beb97f423dd8f146e0ee10718cc Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 19 Jan 2023 11:03:40 +0100 Subject: [PATCH 23/70] Add tests for direct provider error handle --- .../connection-provider-direct.test.js | 79 +++++++++++++++++-- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index f04ecbbb7..87e13429a 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -56,10 +56,11 @@ describe('#unit DirectConnectionProvider', () => { expect(conn instanceof DelegateConnection).toBeTruthy() }) - it('should not purge connections for address when AuthorizationExpired happens', async () => { + it('should close connection and remove authToken for address when AuthorizationExpired happens', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() jest.spyOn(pool, 'purge') + jest.spyOn(pool, 'apply') const connectionProvider = newDirectConnectionProvider(address, pool) const conn = await connectionProvider.acquireConnection({ @@ -72,12 +73,48 @@ describe('#unit DirectConnectionProvider', () => { 'Neo.ClientError.Security.AuthorizationExpired' ) + jest.spyOn(conn, 'close') + conn.handleAndTransformError(error, address) + expect(conn.close).toHaveBeenCalled() expect(pool.purge).not.toHaveBeenCalledWith(address) + expect(pool.apply).toHaveBeenCalledTimes(1) + + const [[calledAddress, appliedFunction]] = pool.apply.mock.calls + + expect(calledAddress).toBe(address) + + const fakeConn = { authToken: 'some token' } + + appliedFunction(fakeConn) + expect(fakeConn.authToken).toBe(null) + pool.apply(address, conn => expect(conn.authToken).toBe(null)) + }) + + it('should call authenticationAuthProvider.handleError when AuthorizationExpired happens', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connectionProvider = newDirectConnectionProvider(address, pool) + + const handleError = jest.spyOn(connectionProvider._authenticationProvider, 'handleError') + + const conn = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: '' + }) + + const error = newError( + 'Message', + 'Neo.ClientError.Security.AuthorizationExpired' + ) + + conn.handleAndTransformError(error, address) + + expect(handleError).toBeCalledWith({ connection: conn, code: 'Neo.ClientError.Security.AuthorizationExpired' }) }) - it('should purge not change error when AuthorizationExpired happens', async () => { + it('should not change error when AuthorizationExpired happens', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() const connectionProvider = newDirectConnectionProvider(address, pool) @@ -98,10 +135,11 @@ describe('#unit DirectConnectionProvider', () => { }) }) -it('should not purge connections for address when TokenExpired happens', async () => { +it('should close the connection when TokenExpired happens', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() jest.spyOn(pool, 'purge') + jest.spyOn(pool, 'apply') const connectionProvider = newDirectConnectionProvider(address, pool) const conn = await connectionProvider.acquireConnection({ @@ -114,9 +152,14 @@ it('should not purge connections for address when TokenExpired happens', async ( 'Neo.ClientError.Security.TokenExpired' ) + jest.spyOn(conn, 'close') + conn.handleAndTransformError(error, address) + expect(conn.close).toHaveBeenCalled() expect(pool.purge).not.toHaveBeenCalledWith(address) + expect(pool.apply).toHaveBeenCalledTimes(0) + pool.apply(address, conn => expect(conn.authToken).toBeDefined()) }) it('should not change error when TokenExpired happens', async () => { @@ -139,6 +182,28 @@ it('should not change error when TokenExpired happens', async () => { expect(error).toBe(expectedError) }) +it('should call authenticationAuthProvider.handleError when TokenExpired happens', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connectionProvider = newDirectConnectionProvider(address, pool) + + const handleError = jest.spyOn(connectionProvider._authenticationProvider, 'handleError') + + const conn = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: '' + }) + + const error = newError( + 'Message', + 'Neo.ClientError.Security.TokenExpired' + ) + + conn.handleAndTransformError(error, address) + + expect(handleError).toBeCalledWith({ connection: conn, code: 'Neo.ClientError.Security.TokenExpired' }) +}) + describe('.verifyConnectivityAndGetServerInfo()', () => { describe('when connection is available in the pool', () => { it('should return the server info', async () => { @@ -325,11 +390,12 @@ function newDirectConnectionProvider (address, pool) { } function newPool ({ create, config } = {}) { + const auth = { scheme: 'bearer', credentials: 'my token' } const _create = (address, release) => { if (create) { return create(address, release) } - return new FakeConnection(address, release) + return new FakeConnection(address, release, undefined, auth) } return new Pool({ config, @@ -339,12 +405,13 @@ function newPool ({ create, config } = {}) { } class FakeConnection extends Connection { - constructor (address, release, server) { + constructor (address, release, server, auth) { super(null) this._address = address this._release = jest.fn(() => release(address, this)) this._server = server + this._authToken = auth } get authToken () { @@ -352,7 +419,7 @@ class FakeConnection extends Connection { } set authToken (authToken) { - this._authToken = this.authToken + this._authToken = authToken } get address () { From e1354d2e8ece7d1f92d5820f285b690f19eaf2ed Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 19 Jan 2023 11:57:52 +0100 Subject: [PATCH 24/70] Add tests for handle error in the RoutingConnectionProvider --- .../connection-provider-routing.test.js | 122 +++++++++++++++++- 1 file changed, 119 insertions(+), 3 deletions(-) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 7dcea43bd..ae6a09521 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -1434,10 +1434,11 @@ describe.each([ }) }, 10000) - it.each(usersDataSet)('should not purge connections for address when AuthorizationExpired happens [user=%s]', async (user) => { + it.each(usersDataSet)('should close connection and erase authToken for connection with address when AuthorizationExpired happens [user=%s]', async (user) => { const pool = newPool() jest.spyOn(pool, 'purge') + jest.spyOn(pool, 'apply') const connectionProvider = newRoutingConnectionProvider( [ @@ -1468,11 +1469,78 @@ describe.each([ impersonatedUser: user }) + jest.spyOn(server2Connection, 'close') + jest.spyOn(server3Connection, 'close') + server3Connection.handleAndTransformError(error, server3) server2Connection.handleAndTransformError(error, server2) + expect(server2Connection.close).toHaveBeenCalled() + expect(server3Connection.close).toHaveBeenCalled() + expect(pool.purge).not.toHaveBeenCalledWith(server3) expect(pool.purge).not.toHaveBeenCalledWith(server2) + + expect(pool.apply).toHaveBeenCalledTimes(2) + + const [[ + calledAddress1, appliedFunction1 + ], + [ + calledAddress2, appliedFunction2 + ]] = pool.apply.mock.calls + + expect(calledAddress1).toBe(server3) + let fakeConn = { authToken: 'some token' } + appliedFunction1(fakeConn) + expect(fakeConn.authToken).toBe(null) + pool.apply(server3, conn => expect(conn.authToken).toBe(null)) + + expect(calledAddress2).toBe(server2) + fakeConn = { authToken: 'some token' } + appliedFunction2(fakeConn) + expect(fakeConn.authToken).toBe(null) + pool.apply(server2, conn => expect(conn.authToken).toBe(null)) + }) + + it.each(usersDataSet)('should call authenticationAuthProvider.handleError when AuthorizationExpired happens [user=%s]', async (user) => { + const pool = newPool() + const connectionProvider = newRoutingConnectionProvider( + [ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + + const handleError = jest.spyOn(connectionProvider._authenticationProvider, 'handleError') + + const error = newError( + 'Message', + 'Neo.ClientError.Security.AuthorizationExpired' + ) + + const server2Connection = await connectionProvider.acquireConnection({ + accessMode: 'WRITE', + database: null, + impersonatedUser: user + }) + + const server3Connection = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: null, + impersonatedUser: user + }) + + server3Connection.handleAndTransformError(error, server3) + server2Connection.handleAndTransformError(error, server2) + + expect(handleError).toBeCalledWith({ connection: server3Connection, code: 'Neo.ClientError.Security.AuthorizationExpired' }) + expect(handleError).toBeCalledWith({ connection: server2Connection, code: 'Neo.ClientError.Security.AuthorizationExpired' }) }) it.each(usersDataSet)('should purge not change error when AuthorizationExpired happens [user=%s]', async (user) => { @@ -1515,6 +1583,7 @@ describe.each([ const pool = newPool() jest.spyOn(pool, 'purge') + jest.spyOn(pool, 'apply') const connectionProvider = newRoutingConnectionProvider( [ @@ -1550,6 +1619,49 @@ describe.each([ expect(pool.purge).not.toHaveBeenCalledWith(server3) expect(pool.purge).not.toHaveBeenCalledWith(server2) + expect(pool.apply).toHaveBeenCalledTimes(0) + pool.apply(server2, conn => expect(conn.authToken).toBeDefined()) + pool.apply(server3, conn => expect(conn.authToken).toBeDefined()) + }) + + it.each(usersDataSet)('should call authenticationAuthProvider.handleError when TokenExpired happens [user=%s]', async (user) => { + const pool = newPool() + const connectionProvider = newRoutingConnectionProvider( + [ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + + const handleError = jest.spyOn(connectionProvider._authenticationProvider, 'handleError') + + const error = newError( + 'Message', + 'Neo.ClientError.Security.TokenExpired' + ) + + const server2Connection = await connectionProvider.acquireConnection({ + accessMode: 'WRITE', + database: null, + impersonatedUser: user + }) + + const server3Connection = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: null, + impersonatedUser: user + }) + + server3Connection.handleAndTransformError(error, server3) + server2Connection.handleAndTransformError(error, server2) + + expect(handleError).toBeCalledWith({ connection: server3Connection, code: 'Neo.ClientError.Security.TokenExpired' }) + expect(handleError).toBeCalledWith({ connection: server2Connection, code: 'Neo.ClientError.Security.TokenExpired' }) }) it.each(usersDataSet)('should not change error when TokenExpired happens [user=%s]', async (user) => { @@ -2943,7 +3055,10 @@ describe.each([ return Promise.reject(e) } } - return Promise.resolve(new FakeConnection(address, release, 'version', PROTOCOL_VERSION)) + return Promise.resolve(new FakeConnection(address, release, 'version', PROTOCOL_VERSION, undefined, { + scheme: 'bearer', + credentials: 'token' + })) } return new Pool({ config, @@ -3093,7 +3208,7 @@ function expectPoolToNotContain (pool, addresses) { } class FakeConnection extends Connection { - constructor (address, release, version, protocolVersion, server) { + constructor (address, release, version, protocolVersion, server, authToken) { super(null) this._address = address @@ -3103,6 +3218,7 @@ class FakeConnection extends Connection { this._release = jest.fn(() => release(address, this)) this.resetAndFlush = jest.fn(() => Promise.resolve()) this._server = server + this._authToken = authToken } get authToken () { From cb3aec1c6dc3d331faee66892666ac0f223e3e0a Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 19 Jan 2023 14:26:57 +0100 Subject: [PATCH 25/70] Add test for 5.1 in RoutingProvider --- .../connection-provider/connection-provider-routing.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index ae6a09521..da18cd351 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -48,7 +48,8 @@ describe.each([ 4.2, 4.3, 4.4, - 5.0 + 5.0, + 5.1 ])('#unit RoutingConnectionProvider (PROTOCOL_VERSION=%d)', (PROTOCOL_VERSION) => { const server0 = ServerAddress.fromUrl('server0') const server1 = ServerAddress.fromUrl('server1') From 19f3633524cf08f15928414d15888f2c76715f21 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 19 Jan 2023 16:09:35 +0100 Subject: [PATCH 26/70] Add test for RoutingConnectionProvider resource creation setup (pool) --- .../connection-provider-pooled.js | 4 +- .../connection-provider-routing.js | 5 +- .../connection-provider-routing.test.js | 104 +++++++++++++++++- 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 7ea71079a..e6d86b174 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -25,7 +25,7 @@ import AuthenticationProvider from './authentication-provider' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { constructor ( - { id, config, log, userAgent, authTokenProvider }, + { id, config, log, userAgent, authTokenProvider, newPool = (...args) => new Pool(...args) }, createChannelConnectionHook = null ) { super() @@ -44,7 +44,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._log ) }) - this._connectionPool = new Pool({ + this._connectionPool = newPool({ create: this._createConnection.bind(this), destroy: this._destroyConnection.bind(this), validateOnAcquire: this._validateConnectionOnAcquire.bind(this), diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index cbe3e6fde..559028cea 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -67,9 +67,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider log, userAgent, authTokenProvider, - routingTablePurgeDelay + routingTablePurgeDelay, + newPool }) { - super({ id, config, log, userAgent, authTokenProvider }, address => { + super({ id, config, log, userAgent, authTokenProvider, newPool }, address => { return createChannelConnection( address, this._config, diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index da18cd351..e5f64ac75 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -31,6 +31,7 @@ import { Pool } from '../../src/pool' import SimpleHostNameResolver from '../../src/channel/browser/browser-host-name-resolver' import RoutingConnectionProvider from '../../src/connection-provider/connection-provider-routing' import { DelegateConnection, Connection } from '../../src/connection' +import AuthenticationProvider from '../../src/connection-provider/authentication-provider' const { serverAddress: { ServerAddress }, @@ -3047,6 +3048,101 @@ describe.each([ }) }) + describe('constructor', () => { + describe('newPool', () => { + describe('param.create', () => { + it('should create connection', async () => { + const { create, createChannelConnectionHook, provider } = setup() + + const connection = await create({}, server0, undefined) + + expect(createChannelConnectionHook).toHaveBeenCalledWith(server0) + expect(provider._openConnections[connection.id]).toBe(connection) + await expect(createChannelConnectionHook.mock.results[0].value).resolves.toBe(connection) + }) + + it('should register the release function into the connection', async () => { + const { create } = setup() + const releaseResult = { property: 'some property' } + const release = jest.fn(() => releaseResult) + + const connection = await create({}, server0, release) + + const released = connection._release() + + expect(released).toBe(releaseResult) + expect(release).toHaveBeenCalledWith(server0, connection) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should authenticate connection (auth = %o)', async (auth) => { + const { create, authenticationProviderHook } = setup() + + const connection = await create({ auth }, server0) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, + auth + }) + }) + + it('should handle create connection failures', async () => { + const error = newError('some error') + const createConnection = jest.fn(() => Promise.reject(error)) + const { create, authenticationProviderHook, provider } = setup({ createConnection }) + const openConnections = { ...provider._openConnections } + + await expect(create({}, server0)).rejects.toThrow(error) + + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + expect(provider._openConnections).toEqual(openConnections) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should handle authentication failures (auth = %o)', async (auth) => { + const error = newError('some error') + const authenticationProvider = jest.fn(() => Promise.reject(error)) + const { create, authenticationProviderHook, createChannelConnectionHook, provider } = setup({ authenticationProvider }) + const openConnections = { ...provider._openConnections } + + await expect(create({ auth }, server0)).rejects.toThrow(error) + + const connection = await createChannelConnectionHook.mock.results[0].value + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ auth, connection }) + expect(provider._openConnections).toEqual(openConnections) + expect(connection._closed).toBe(true) + }) + }) + + function setup ({ createConnection, authenticationProvider } = {}) { + const newPool = jest.fn((...args) => new Pool(...args)) + const createChannelConnectionHook = createConnection || jest.fn(async (address) => new FakeConnection(address)) + const authenticationProviderHook = new AuthenticationProvider({ }) + jest.spyOn(authenticationProviderHook, 'authenticate') + .mockImplementation(authenticationProvider || jest.fn(({ connection }) => Promise.resolve(connection))) + const provider = new RoutingConnectionProvider({ + newPool, + config: {}, + address: server01 + }) + provider._createChannelConnection = createChannelConnectionHook + provider._authenticationProvider = authenticationProviderHook + return { + provider, + ...newPool.mock.calls[0][0], + createChannelConnectionHook, + authenticationProviderHook + } + } + }) + }) + function newPool ({ create, config } = {}) { const _create = (address, release) => { if (create) { @@ -3220,6 +3316,12 @@ class FakeConnection extends Connection { this.resetAndFlush = jest.fn(() => Promise.resolve()) this._server = server this._authToken = authToken + this._id = 1 + this._closed = false + } + + get id () { + return this._id } get authToken () { @@ -3243,7 +3345,7 @@ class FakeConnection extends Connection { } async close () { - + this._closed = true } protocol () { From a80b8692f406201a245d23d290013d3bda2b2a6e Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 19 Jan 2023 16:38:52 +0100 Subject: [PATCH 27/70] Add tests to the RoutingConnectionProvider pool configuration --- .../connection-provider-pooled.js | 2 +- .../connection-provider-routing.test.js | 144 +++++++++++++++++- 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index e6d86b174..648df225f 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -95,7 +95,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { await this._authenticationProvider.authenticate({ connection: conn, auth }) return true } catch (error) { - this._log.info( + this._log.debug( `The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'` ) return false diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index e5f64ac75..fafead1d2 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3120,16 +3120,147 @@ describe.each([ }) }) - function setup ({ createConnection, authenticationProvider } = {}) { + describe('param.destroy', () => { + it('should close connection and unregister it', async () => { + const { create, destroy, provider } = setup() + const openConnections = { ...provider._openConnections } + const connection = await create({}, server0, undefined) + + await destroy(connection) + + expect(connection._closed).toBe(true) + expect(provider._openConnections).toEqual(openConnections) + }) + }) + + describe('param.validateOnAcquire', () => { + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should return true when connection is open and within the lifetime and authentication succeed (auth=%o)', async (auth) => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + + const { validateOnAcquire, authenticationProviderHook } = setup() + + await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(true) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, auth + }) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should return true when connection is open and within the lifetime and authentication fails (auth=%o)', async (auth) => { + const connection = new FakeConnection(server0) + const error = newError('failed') + const authenticationProvider = jest.fn(() => Promise.reject(error)) + connection.creationTimestamp = Date.now() + + const { validateOnAcquire, authenticationProviderHook, log } = setup({ authenticationProvider }) + + await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(false) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, auth + }) + + expect(log.debug).toHaveBeenCalledWith( + `The connection ${connection.id} is not valid because of an error ${error.code} '${error.message}'` + ) + }) + it('should return false when connection is closed and within the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + await connection.close() + + const { validateOnAcquire, authenticationProviderHook } = setup() + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + + it('should return false when connection is open and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 }) + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + + it('should return false when connection is closed and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + await connection.close() + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 }) + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + }) + + describe('param.validateOnRelease', () => { + it('should return true when connection is open and within the lifetime', () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + + const { validateOnRelease } = setup() + + expect(validateOnRelease(connection)).toBe(true) + }) + + it('should return false when connection is closed and within the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + await connection.close() + + const { validateOnRelease } = setup() + + expect(validateOnRelease(connection)).toBe(false) + }) + + it('should return false when connection is open and out of the lifetime', () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnRelease } = setup({ maxConnectionLifetime: 3000 }) + + expect(validateOnRelease(connection)).toBe(false) + }) + + it('should return false when connection is closed and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + await connection.close() + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnRelease } = setup({ maxConnectionLifetime: 3000 }) + + expect(validateOnRelease(connection)).toBe(false) + }) + }) + + function setup ({ createConnection, authenticationProvider, maxConnectionLifetime } = {}) { const newPool = jest.fn((...args) => new Pool(...args)) + const log = new Logger('debug', () => undefined) + jest.spyOn(log, 'debug') const createChannelConnectionHook = createConnection || jest.fn(async (address) => new FakeConnection(address)) const authenticationProviderHook = new AuthenticationProvider({ }) jest.spyOn(authenticationProviderHook, 'authenticate') .mockImplementation(authenticationProvider || jest.fn(({ connection }) => Promise.resolve(connection))) const provider = new RoutingConnectionProvider({ newPool, - config: {}, - address: server01 + config: { + maxConnectionLifetime: maxConnectionLifetime || 1000 + }, + address: server01, + log }) provider._createChannelConnection = createChannelConnectionHook provider._authenticationProvider = authenticationProviderHook @@ -3137,7 +3268,8 @@ describe.each([ provider, ...newPool.mock.calls[0][0], createChannelConnectionHook, - authenticationProviderHook + authenticationProviderHook, + log } } }) @@ -3348,6 +3480,10 @@ class FakeConnection extends Connection { this._closed = true } + isOpen () { + return !this._closed + } + protocol () { return { version: this._protocolVersion, From 46634684f8da69527e92e5f54bc650147e9e1eab Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 19 Jan 2023 16:45:35 +0100 Subject: [PATCH 28/70] Add tests to the DirectConnectionProvider pool configuration --- .../connection-provider-direct.js | 4 +- .../connection-provider-direct.test.js | 241 ++++++++++++++++++ 2 files changed, 243 insertions(+), 2 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 9b958e1a2..7ea4d1f9e 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -33,8 +33,8 @@ const { const { SERVICE_UNAVAILABLE } = error export default class DirectConnectionProvider extends PooledConnectionProvider { - constructor ({ id, config, log, address, userAgent, authTokenProvider }) { - super({ id, config, log, userAgent, authTokenProvider }) + constructor ({ id, config, log, address, userAgent, authTokenProvider, newPool }) { + super({ id, config, log, userAgent, authTokenProvider, newPool }) this._address = address } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 87e13429a..ae685c871 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -21,6 +21,7 @@ import DirectConnectionProvider from '../../src/connection-provider/connection-p import { Pool } from '../../src/pool' import { Connection, DelegateConnection } from '../../src/connection' import { internal, newError, ServerInfo } from 'neo4j-driver-core' +import AuthenticationProvider from '../../src/connection-provider/authentication-provider' const { serverAddress: { ServerAddress }, @@ -376,6 +377,236 @@ describe('.verifyConnectivityAndGetServerInfo()', () => { } }) }) + + describe('constructor', () => { + describe('newPool', () => { + const server0 = ServerAddress.fromUrl('localhost:123') + const server01 = ServerAddress.fromUrl('localhost:1235') + + describe('param.create', () => { + it('should create connection', async () => { + const { create, createChannelConnectionHook, provider } = setup() + + const connection = await create({}, server0, undefined) + + expect(createChannelConnectionHook).toHaveBeenCalledWith(server0) + expect(provider._openConnections[connection.id]).toBe(connection) + await expect(createChannelConnectionHook.mock.results[0].value).resolves.toBe(connection) + }) + + it('should register the release function into the connection', async () => { + const { create } = setup() + const releaseResult = { property: 'some property' } + const release = jest.fn(() => releaseResult) + + const connection = await create({}, server0, release) + + const released = connection._release() + + expect(released).toBe(releaseResult) + expect(release).toHaveBeenCalledWith(server0, connection) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should authenticate connection (auth = %o)', async (auth) => { + const { create, authenticationProviderHook } = setup() + + const connection = await create({ auth }, server0) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, + auth + }) + }) + + it('should handle create connection failures', async () => { + const error = newError('some error') + const createConnection = jest.fn(() => Promise.reject(error)) + const { create, authenticationProviderHook, provider } = setup({ createConnection }) + const openConnections = { ...provider._openConnections } + + await expect(create({}, server0)).rejects.toThrow(error) + + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + expect(provider._openConnections).toEqual(openConnections) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should handle authentication failures (auth = %o)', async (auth) => { + const error = newError('some error') + const authenticationProvider = jest.fn(() => Promise.reject(error)) + const { create, authenticationProviderHook, createChannelConnectionHook, provider } = setup({ authenticationProvider }) + const openConnections = { ...provider._openConnections } + + await expect(create({ auth }, server0)).rejects.toThrow(error) + + const connection = await createChannelConnectionHook.mock.results[0].value + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ auth, connection }) + expect(provider._openConnections).toEqual(openConnections) + expect(connection._closed).toBe(true) + }) + }) + + describe('param.destroy', () => { + it('should close connection and unregister it', async () => { + const { create, destroy, provider } = setup() + const openConnections = { ...provider._openConnections } + const connection = await create({}, server0, undefined) + + await destroy(connection) + + expect(connection._closed).toBe(true) + expect(provider._openConnections).toEqual(openConnections) + }) + }) + + describe('param.validateOnAcquire', () => { + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should return true when connection is open and within the lifetime and authentication succeed (auth=%o)', async (auth) => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + + const { validateOnAcquire, authenticationProviderHook } = setup() + + await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(true) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, auth + }) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should return true when connection is open and within the lifetime and authentication fails (auth=%o)', async (auth) => { + const connection = new FakeConnection(server0) + const error = newError('failed') + const authenticationProvider = jest.fn(() => Promise.reject(error)) + connection.creationTimestamp = Date.now() + + const { validateOnAcquire, authenticationProviderHook, log } = setup({ authenticationProvider }) + + await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(false) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, auth + }) + + expect(log.debug).toHaveBeenCalledWith( + `The connection ${connection.id} is not valid because of an error ${error.code} '${error.message}'` + ) + }) + it('should return false when connection is closed and within the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + await connection.close() + + const { validateOnAcquire, authenticationProviderHook } = setup() + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + + it('should return false when connection is open and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 }) + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + + it('should return false when connection is closed and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + await connection.close() + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 }) + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + }) + + describe('param.validateOnRelease', () => { + it('should return true when connection is open and within the lifetime', () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + + const { validateOnRelease } = setup() + + expect(validateOnRelease(connection)).toBe(true) + }) + + it('should return false when connection is closed and within the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + await connection.close() + + const { validateOnRelease } = setup() + + expect(validateOnRelease(connection)).toBe(false) + }) + + it('should return false when connection is open and out of the lifetime', () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnRelease } = setup({ maxConnectionLifetime: 3000 }) + + expect(validateOnRelease(connection)).toBe(false) + }) + + it('should return false when connection is closed and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + await connection.close() + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnRelease } = setup({ maxConnectionLifetime: 3000 }) + + expect(validateOnRelease(connection)).toBe(false) + }) + }) + + function setup ({ createConnection, authenticationProvider, maxConnectionLifetime } = {}) { + const newPool = jest.fn((...args) => new Pool(...args)) + const log = new Logger('debug', () => undefined) + jest.spyOn(log, 'debug') + const createChannelConnectionHook = createConnection || jest.fn(async (address) => new FakeConnection(address)) + const authenticationProviderHook = new AuthenticationProvider({ }) + jest.spyOn(authenticationProviderHook, 'authenticate') + .mockImplementation(authenticationProvider || jest.fn(({ connection }) => Promise.resolve(connection))) + const provider = new DirectConnectionProvider({ + newPool, + config: { + maxConnectionLifetime: maxConnectionLifetime || 1000 + }, + address: server01, + log + }) + provider._createChannelConnection = createChannelConnectionHook + provider._authenticationProvider = authenticationProviderHook + return { + provider, + ...newPool.mock.calls[0][0], + createChannelConnectionHook, + authenticationProviderHook, + log + } + } + }) + }) }) function newDirectConnectionProvider (address, pool) { @@ -412,6 +643,12 @@ class FakeConnection extends Connection { this._release = jest.fn(() => release(address, this)) this._server = server this._authToken = auth + this._closed = false + this._id = 1 + } + + get id () { + return this._id } get authToken () { @@ -431,6 +668,10 @@ class FakeConnection extends Connection { } async close () { + this._closed = true + } + isOpen () { + return !this._closed } } From b5a6b6cfe9947ad1610c2dd083b3d4a43d54b3e8 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Fri, 20 Jan 2023 10:52:48 +0100 Subject: [PATCH 29/70] Small fix and sync deno --- packages/bolt-connection/src/lang/object.js | 4 ++++ .../connection-provider/connection-provider-direct.js | 4 ++-- .../connection-provider/connection-provider-pooled.js | 6 +++--- .../connection-provider/connection-provider-routing.js | 5 +++-- .../neo4j-driver-deno/lib/bolt-connection/lang/object.js | 4 ++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/bolt-connection/src/lang/object.js b/packages/bolt-connection/src/lang/object.js index a8d7f29cb..e2a862c04 100644 --- a/packages/bolt-connection/src/lang/object.js +++ b/packages/bolt-connection/src/lang/object.js @@ -22,6 +22,10 @@ export function equals (a, b) { return true } + if (a === null || b === null) { + return false + } + if (typeof a === 'object' && typeof b === 'object') { const keysA = Object.keys(a) const keysB = Object.keys(b) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 3fbecec45..2af06144f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -33,8 +33,8 @@ const { const { SERVICE_UNAVAILABLE } = error export default class DirectConnectionProvider extends PooledConnectionProvider { - constructor ({ id, config, log, address, userAgent, authTokenProvider }) { - super({ id, config, log, userAgent, authTokenProvider }) + constructor ({ id, config, log, address, userAgent, authTokenProvider, newPool }) { + super({ id, config, log, userAgent, authTokenProvider, newPool }) this._address = address } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 2990eb5c7..6faa41d3d 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -25,7 +25,7 @@ import AuthenticationProvider from './authentication-provider.js' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { constructor ( - { id, config, log, userAgent, authTokenProvider }, + { id, config, log, userAgent, authTokenProvider, newPool = (...args) => new Pool(...args) }, createChannelConnectionHook = null ) { super() @@ -44,7 +44,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._log ) }) - this._connectionPool = new Pool({ + this._connectionPool = newPool({ create: this._createConnection.bind(this), destroy: this._destroyConnection.bind(this), validateOnAcquire: this._validateConnectionOnAcquire.bind(this), @@ -95,7 +95,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { await this._authenticationProvider.authenticate({ connection: conn, auth }) return true } catch (error) { - this._log.info( + this._log.debug( `The connection ${conn.id} is not valid because of an error ${error.code} '${error.message}'` ) return false diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 00cc7af13..569d84f5e 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -67,9 +67,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider log, userAgent, authTokenProvider, - routingTablePurgeDelay + routingTablePurgeDelay, + newPool }) { - super({ id, config, log, userAgent, authTokenProvider }, address => { + super({ id, config, log, userAgent, authTokenProvider, newPool }, address => { return createChannelConnection( address, this._config, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/lang/object.js b/packages/neo4j-driver-deno/lib/bolt-connection/lang/object.js index a8d7f29cb..e2a862c04 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/lang/object.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/lang/object.js @@ -22,6 +22,10 @@ export function equals (a, b) { return true } + if (a === null || b === null) { + return false + } + if (typeof a === 'object' && typeof b === 'object') { const keysA = Object.keys(a) const keysB = Object.keys(b) From 1951038a26c28b0a1cf4eadf5af904915e5abc3e Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Fri, 27 Jan 2023 16:13:18 +0100 Subject: [PATCH 30/70] Fix merge issues --- packages/testkit-backend/src/request-handlers-rx.js | 1 - packages/testkit-backend/src/request-handlers.js | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 00e78690f..5879f8b3e 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -26,7 +26,6 @@ export { NewAuthTokenProvider, AuthTokenProviderCompleted, AuthTokenProviderClose, - StartSubTest, FakeTimeInstall, FakeTimeTick, FakeTimeUninstall diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 110f6decc..2ea3c7cf3 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -626,10 +626,6 @@ export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config .catch(e => wire.writeError(e)) } -export function FakeTimeInstall (_, context, _data, wire) { - context.clock = sinon.useFakeTimers(new Date().getTime()) -} - export function FakeTimeInstall ({ mock }, context, _data, wire) { context.clock = new mock.FakeTime() wire.writeResponse(responses.FakeTimeAck()) From 792c4a513c10f9e62c7d1dc30f594a8ef22faf50 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 1 Feb 2023 17:36:59 +0100 Subject: [PATCH 31/70] Implement user-switch doesn't allow sticky connection --- .../connection-provider-direct.js | 11 +- .../connection-provider-pooled.js | 16 +- .../connection-provider-routing.js | 79 +++++---- packages/bolt-connection/src/pool/pool.js | 1 - .../connection-provider-direct.test.js | 31 ++++ .../connection-provider-routing.test.js | 155 ++++++++++++++++++ packages/core/src/connection-provider.ts | 1 + .../connection-provider-direct.js | 11 +- .../connection-provider-pooled.js | 16 +- .../connection-provider-routing.js | 79 +++++---- .../lib/bolt-connection/pool/pool.js | 1 - .../lib/core/connection-provider.ts | 1 + .../connection-provider-pooled.test.js | 2 +- 13 files changed, 325 insertions(+), 79 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 7ea4d1f9e..4191aa14b 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -24,7 +24,6 @@ import { ConnectionErrorHandler } from '../connection' import { internal, error } from 'neo4j-driver-core' -import { object } from '../lang' const { constants: { BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, BOLT_PROTOCOL_V4_4 } @@ -43,7 +42,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, auth } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, auth, allowStickyConnection } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, handleAuthorizationExpired: (error, address, conn) => @@ -52,9 +51,11 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { const connection = await this._connectionPool.acquire({ auth }, this._address) - if (auth && !object.equals(auth, connection.authToken)) { - await connection._release() - return await this._createStickyConnection({ address: this._address, auth }) + if (auth) { + const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + if (stickyConnection) { + return stickyConnection + } } return new DelegateConnection(connection, databaseSpecificErrorHandler) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 648df225f..59da5dffc 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -19,8 +19,9 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection' import Pool, { PoolConfig } from '../pool' -import { error, ConnectionProvider, ServerInfo } from 'neo4j-driver-core' +import { error, ConnectionProvider, ServerInfo, newError } from 'neo4j-driver-core' import AuthenticationProvider from './authentication-provider' +import { object } from '../lang' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { @@ -167,6 +168,19 @@ export default class PooledConnectionProvider extends ConnectionProvider { return serverInfo } + async _getStickyConnection ({ auth, connection, allowStickyConnection }) { + const connectionWithSameCredentials = object.equals(auth, connection.authToken) + const shouldCreateStickyConnection = !connectionWithSameCredentials + const stickyConnectionWasCreated = connectionWithSameCredentials && !connection.supportsReAuth + if (allowStickyConnection !== true && (shouldCreateStickyConnection || stickyConnectionWasCreated)) { + await connection._release() + throw newError('Driver is connected to a database that does not support user switch.') + } else if (allowStickyConnection === true && shouldCreateStickyConnection) { + await connection._release() + return await this._createStickyConnection({ address: this._address, auth }) + } + } + async close () { // purge all idle connections in the connection pool await this._connectionPool.close() diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 559028cea..011cb4673 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -28,7 +28,6 @@ import { ConnectionErrorHandler, DelegateConnection } from '../connection' -import { object } from '../lang' const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error const { @@ -52,6 +51,7 @@ const AUTHORIZATION_EXPIRED_CODE = const INVALID_ARGUMENT_ERROR = 'Neo.ClientError.Statement.ArgumentError' const INVALID_REQUEST_ERROR = 'Neo.ClientError.Request.Invalid' const STATEMENT_TYPE_ERROR = 'Neo.ClientError.Statement.TypeError' +const NOT_AVAILABLE = 'N/A' const SYSTEM_DB_NAME = 'system' const DEFAULT_DB_NAME = null @@ -143,7 +143,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection } = {}) { let name let address const context = { database: database || DEFAULT_DB_NAME } @@ -162,6 +162,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, auth, + allowStickyConnection, onDatabaseNameResolved: (databaseName) => { context.database = context.database || databaseName if (onDatabaseNameResolved) { @@ -192,9 +193,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider try { const connection = await this._connectionPool.acquire({ auth }, address) - if (auth && !object.equals(auth, connection.authToken)) { - await connection._release() - return await this._createStickyConnection({ address, auth }) + if (auth) { + const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + if (stickyConnection) { + return stickyConnection + } } return new DelegateConnection(connection, databaseSpecificErrorHandler) @@ -312,7 +315,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { + _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection } = {}) { const currentRoutingTable = this._routingTableRegistry.get( database, () => new RoutingTable({ database }) @@ -324,10 +327,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._log.info( `Routing table is stale for database: "${database}" and access mode: "${accessMode}": ${currentRoutingTable}` ) - return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) + return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection) } - _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) { + _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection) { const knownRouters = currentRoutingTable.routers if (this._useSeedRouter) { @@ -337,7 +340,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth + auth, + allowStickyConnection ) } return this._fetchRoutingTableFromKnownRoutersFallbackToSeedRouter( @@ -346,7 +350,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth + auth, + allowStickyConnection ) } @@ -356,7 +361,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth + auth, + allowStickyConnection ) { // we start with seed router, no routers were probed before const seenRouters = [] @@ -366,7 +372,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) if (newRoutingTable) { @@ -378,7 +385,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) newRoutingTable = newRoutingTable2 error = error2 || error @@ -398,14 +406,16 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth + auth, + allowStickyConnection ) { let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters( knownRouters, currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) if (!newRoutingTable) { @@ -416,7 +426,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) } @@ -433,14 +444,16 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) { const [newRoutingTable, error] = await this._fetchRoutingTable( knownRouters, currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) if (newRoutingTable) { @@ -466,7 +479,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider routingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) { const resolvedAddresses = await this._resolveSeedRouter(seedRouter) @@ -475,7 +489,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider address => seenRouters.indexOf(address) < 0 ) - return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth) + return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth, allowStickyConnection) } async _resolveSeedRouter (seedRouter) { @@ -487,7 +501,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return [].concat.apply([], dnsResolvedAddresses) } - async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth) { + async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth, allowStickyConnection) { return routerAddresses.reduce( async (refreshedTablePromise, currentRouter, currentIndex) => { const [newRoutingTable] = await refreshedTablePromise @@ -511,7 +525,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRouter, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) if (session) { try { @@ -536,16 +551,15 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth) { + async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth, allowStickyConnection) { try { - let connection = await this._connectionPool.acquire({ auth }, routerAddress) - - if (auth && !object.equals(auth, connection.authToken)) { - await connection._release() - connection = await this._createStickyConnection({ - address: routerAddress, - auth - }) + const connection = await this._connectionPool.acquire({ auth }, routerAddress) + + if (auth) { + const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + if (stickyConnection) { + return stickyConnection + } } const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ @@ -737,7 +751,8 @@ function _isFailFastError (error) { INVALID_BOOKMARK_MIXTURE_CODE, INVALID_ARGUMENT_ERROR, INVALID_REQUEST_ERROR, - STATEMENT_TYPE_ERROR + STATEMENT_TYPE_ERROR, + NOT_AVAILABLE ].includes(error.code) } diff --git a/packages/bolt-connection/src/pool/pool.js b/packages/bolt-connection/src/pool/pool.js index 0bf53dd32..bba636ace 100644 --- a/packages/bolt-connection/src/pool/pool.js +++ b/packages/bolt-connection/src/pool/pool.js @@ -245,7 +245,6 @@ class Pool { try { // Invoke callback that creates actual connection resource = await this._create(acquisitionContext, address, (address, resource) => this._release(address, resource, pool)) - pool.pushInUse(resource) resourceAcquired(key, this._activeResourceCounts) if (this._log.isDebugEnabled()) { diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index ae685c871..f7f293560 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -22,6 +22,7 @@ import { Pool } from '../../src/pool' import { Connection, DelegateConnection } from '../../src/connection' import { internal, newError, ServerInfo } from 'neo4j-driver-core' import AuthenticationProvider from '../../src/connection-provider/authentication-provider' +import { functional } from '../../src/lang' const { serverAddress: { ServerAddress }, @@ -607,6 +608,36 @@ describe('.verifyConnectivityAndGetServerInfo()', () => { } }) }) + + describe('user-switching', () => { + describe.each([ + undefined, + false, + null + ])('when allowStickyConnection is %s', (allowStickyConnection) => { + it('should raise and error when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newDirectConnectionProvider(address, pool) + const auth = { other: 'token' } + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, address) + expect(connection._release).toHaveBeenCalled() + }) + }) + }) }) function newDirectConnectionProvider (address, pool) { diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index fafead1d2..1f83af6d7 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -32,6 +32,7 @@ import SimpleHostNameResolver from '../../src/channel/browser/browser-host-name- import RoutingConnectionProvider from '../../src/connection-provider/connection-provider-routing' import { DelegateConnection, Connection } from '../../src/connection' import AuthenticationProvider from '../../src/connection-provider/authentication-provider' +import { functional } from '../../src/lang' const { serverAddress: { ServerAddress }, @@ -3275,6 +3276,160 @@ describe.each([ }) }) + describe('user-switching', () => { + describe.each([ + undefined, + false, + null + ])('when allowStickyConnection is %s', (allowStickyConnection) => { + it('should raise and error when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + const auth = { other: 'token' } + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) + expect(connection._release).toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [expired rt]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + pool, + { + dba: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + ) + connectionProvider._useSeedRouter = false + + const auth = { other: 'token' } + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) + expect(connection._release).toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + { + dba: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + }, + pool + ) + + const auth = { other: 'token' } + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + expect(connection._release).toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [], + {}, + pool + ) + + const auth = { other: 'token' } + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + expect(connection._release).toHaveBeenCalled() + }) + }) + }) + function newPool ({ create, config } = {}) { const _create = (address, release) => { if (create) { diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index 09057a9d0..9c7d14518 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -53,6 +53,7 @@ class ConnectionProvider { impersonatedUser?: string onDatabaseNameResolved?: (databaseName?: string) => void auth?: AuthToken + allowStickyConnection?: boolean }): Promise { throw Error('Not implemented') } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 2af06144f..92a8f4558 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -24,7 +24,6 @@ import { ConnectionErrorHandler } from '../connection/index.js' import { internal, error } from '../../core/index.ts' -import { object } from '../lang/index.js' const { constants: { BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, BOLT_PROTOCOL_V4_4 } @@ -43,7 +42,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, auth } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, auth, allowStickyConnection } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, handleAuthorizationExpired: (error, address, conn) => @@ -52,9 +51,11 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { const connection = await this._connectionPool.acquire({ auth }, this._address) - if (auth && !object.equals(auth, connection.authToken)) { - await connection._release() - return await this._createStickyConnection({ address: this._address, auth }) + if (auth) { + const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + if (stickyConnection) { + return stickyConnection + } } return new DelegateConnection(connection, databaseSpecificErrorHandler) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 6faa41d3d..2017fd5bc 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -19,8 +19,9 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection/index.js' import Pool, { PoolConfig } from '../pool/index.js' -import { error, ConnectionProvider, ServerInfo } from '../../core/index.ts' +import { error, ConnectionProvider, ServerInfo, newError } from '../../core/index.ts' import AuthenticationProvider from './authentication-provider.js' +import { object } from '../lang/index.js' const { SERVICE_UNAVAILABLE } = error export default class PooledConnectionProvider extends ConnectionProvider { @@ -167,6 +168,19 @@ export default class PooledConnectionProvider extends ConnectionProvider { return serverInfo } + async _getStickyConnection ({ auth, connection, allowStickyConnection }) { + const connectionWithSameCredentials = object.equals(auth, connection.authToken) + const shouldCreateStickyConnection = !connectionWithSameCredentials + const stickyConnectionWasCreated = connectionWithSameCredentials && !connection.supportsReAuth + if (allowStickyConnection !== true && (shouldCreateStickyConnection || stickyConnectionWasCreated)) { + await connection._release() + throw newError('Driver is connected to a database that does not support user switch.') + } else if (allowStickyConnection === true && shouldCreateStickyConnection) { + await connection._release() + return await this._createStickyConnection({ address: this._address, auth }) + } + } + async close () { // purge all idle connections in the connection pool await this._connectionPool.close() diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 569d84f5e..dc2ed55c4 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -28,7 +28,6 @@ import { ConnectionErrorHandler, DelegateConnection } from '../connection/index.js' -import { object } from '../lang/index.js' const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error const { @@ -52,6 +51,7 @@ const AUTHORIZATION_EXPIRED_CODE = const INVALID_ARGUMENT_ERROR = 'Neo.ClientError.Statement.ArgumentError' const INVALID_REQUEST_ERROR = 'Neo.ClientError.Request.Invalid' const STATEMENT_TYPE_ERROR = 'Neo.ClientError.Statement.TypeError' +const NOT_AVAILABLE = 'N/A' const SYSTEM_DB_NAME = 'system' const DEFAULT_DB_NAME = null @@ -143,7 +143,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection } = {}) { let name let address const context = { database: database || DEFAULT_DB_NAME } @@ -162,6 +162,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, auth, + allowStickyConnection, onDatabaseNameResolved: (databaseName) => { context.database = context.database || databaseName if (onDatabaseNameResolved) { @@ -192,9 +193,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider try { const connection = await this._connectionPool.acquire({ auth }, address) - if (auth && !object.equals(auth, connection.authToken)) { - await connection._release() - return await this._createStickyConnection({ address, auth }) + if (auth) { + const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + if (stickyConnection) { + return stickyConnection + } } return new DelegateConnection(connection, databaseSpecificErrorHandler) @@ -312,7 +315,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { + _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection } = {}) { const currentRoutingTable = this._routingTableRegistry.get( database, () => new RoutingTable({ database }) @@ -324,10 +327,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._log.info( `Routing table is stale for database: "${database}" and access mode: "${accessMode}": ${currentRoutingTable}` ) - return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) + return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection) } - _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) { + _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection) { const knownRouters = currentRoutingTable.routers if (this._useSeedRouter) { @@ -337,7 +340,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth + auth, + allowStickyConnection ) } return this._fetchRoutingTableFromKnownRoutersFallbackToSeedRouter( @@ -346,7 +350,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth + auth, + allowStickyConnection ) } @@ -356,7 +361,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth + auth, + allowStickyConnection ) { // we start with seed router, no routers were probed before const seenRouters = [] @@ -366,7 +372,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) if (newRoutingTable) { @@ -378,7 +385,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) newRoutingTable = newRoutingTable2 error = error2 || error @@ -398,14 +406,16 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth + auth, + allowStickyConnection ) { let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters( knownRouters, currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) if (!newRoutingTable) { @@ -416,7 +426,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) } @@ -433,14 +444,16 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) { const [newRoutingTable, error] = await this._fetchRoutingTable( knownRouters, currentRoutingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) if (newRoutingTable) { @@ -466,7 +479,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider routingTable, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) { const resolvedAddresses = await this._resolveSeedRouter(seedRouter) @@ -475,7 +489,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider address => seenRouters.indexOf(address) < 0 ) - return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth) + return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth, allowStickyConnection) } async _resolveSeedRouter (seedRouter) { @@ -487,7 +501,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return [].concat.apply([], dnsResolvedAddresses) } - async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth) { + async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth, allowStickyConnection) { return routerAddresses.reduce( async (refreshedTablePromise, currentRouter, currentIndex) => { const [newRoutingTable] = await refreshedTablePromise @@ -511,7 +525,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRouter, bookmarks, impersonatedUser, - auth + auth, + allowStickyConnection ) if (session) { try { @@ -536,16 +551,15 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth) { + async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth, allowStickyConnection) { try { - let connection = await this._connectionPool.acquire({ auth }, routerAddress) - - if (auth && !object.equals(auth, connection.authToken)) { - await connection._release() - connection = await this._createStickyConnection({ - address: routerAddress, - auth - }) + const connection = await this._connectionPool.acquire({ auth }, routerAddress) + + if (auth) { + const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + if (stickyConnection) { + return stickyConnection + } } const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ @@ -737,7 +751,8 @@ function _isFailFastError (error) { INVALID_BOOKMARK_MIXTURE_CODE, INVALID_ARGUMENT_ERROR, INVALID_REQUEST_ERROR, - STATEMENT_TYPE_ERROR + STATEMENT_TYPE_ERROR, + NOT_AVAILABLE ].includes(error.code) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js index af3c002d4..c0451efd3 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js @@ -245,7 +245,6 @@ class Pool { try { // Invoke callback that creates actual connection resource = await this._create(acquisitionContext, address, (address, resource) => this._release(address, resource, pool)) - pool.pushInUse(resource) resourceAcquired(key, this._activeResourceCounts) if (this._log.isDebugEnabled()) { diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts index e0ed37df9..7dfc7547b 100644 --- a/packages/neo4j-driver-deno/lib/core/connection-provider.ts +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -53,6 +53,7 @@ class ConnectionProvider { impersonatedUser?: string onDatabaseNameResolved?: (databaseName?: string) => void auth?: AuthToken + allowStickyConnection?: boolean }): Promise { throw Error('Not implemented') } diff --git a/packages/neo4j-driver/test/internal/connection-provider-pooled.test.js b/packages/neo4j-driver/test/internal/connection-provider-pooled.test.js index d36659237..546baa031 100644 --- a/packages/neo4j-driver/test/internal/connection-provider-pooled.test.js +++ b/packages/neo4j-driver/test/internal/connection-provider-pooled.test.js @@ -34,7 +34,7 @@ describe('#unit PooledConnectionProvider', () => { expect(connectionValid).toBeFalsy() }) - xit('should treat not old open connections as valid', async () => { + it('should treat not old open connections as valid', async () => { const provider = new PooledConnectionProvider({ id: 0, config: { From dc8e45a622c8203b02f164b5396e921a7ac65fdf Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 1 Feb 2023 18:18:35 +0100 Subject: [PATCH 32/70] Improve tests --- .../connection-provider-direct.test.js | 12 +- .../connection-provider-routing.test.js | 243 +++++++++--------- 2 files changed, 131 insertions(+), 124 deletions(-) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index f7f293560..c79437af0 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -615,25 +615,27 @@ describe('.verifyConnectivityAndGetServerInfo()', () => { false, null ])('when allowStickyConnection is %s', (allowStickyConnection) => { - it('should raise and error when try switch user on acquire', async () => { + it.each([ + ['new connection', { other: 'auth' }, { other: 'token' }], + ['old connection', { some: 'auth' }, { other: 'token' }] + ])('should raise and error when try switch user on acquire [%s]', async (_, connAuth, acquireAuth) => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) + const connection = new FakeConnection(address, () => {}, undefined, connAuth) const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newDirectConnectionProvider(address, pool) - const auth = { other: 'token' } const error = await connectionProvider .acquireConnection({ accessMode: 'READ', database: '', allowStickyConnection, - auth + auth: acquireAuth }) .catch(functional.identity) expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, address) + expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address) expect(connection._release).toHaveBeenCalled() }) }) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 1f83af6d7..82c11b5f1 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3282,88 +3282,47 @@ describe.each([ false, null ])('when allowStickyConnection is %s', (allowStickyConnection) => { - it('should raise and error when try switch user on acquire', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProvider([ - newRoutingTable( - null, - [server1, server2], - [server3, server2], - [server2, server4] - ) - ], - pool - ) - const auth = { other: 'token' } - - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: '', - allowStickyConnection, - auth - }) - .catch(functional.identity) - - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) - expect(connection._release).toHaveBeenCalled() - }) - - it('should raise and error when try switch user on acquire [expired rt]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProvider([ - newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4], - int(0) // expired - ) - ], - pool, - { - dba: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] + describe.each([ + ['new connection', { other: 'auth' }, { other: 'token' }], + ['old connection', { some: 'auth' }, { other: 'token' }] + ])('%s', (_, connAuth, acquireAuth) => { + it('should raise and error when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool ) - } - ) - connectionProvider._useSeedRouter = false - - const auth = { other: 'token' } - - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) - .catch(functional.identity) + const auth = acquireAuth + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth + }) + .catch(functional.identity) - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) - expect(connection._release).toHaveBeenCalled() - }) + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) + expect(connection._release).toHaveBeenCalled() + }) - it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProviderWithSeedRouter( - server0, - [server0], - [ + it('should raise and error when try switch user on acquire [expired rt]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ newRoutingTable( 'dba', [server1, server2], @@ -3372,6 +3331,7 @@ describe.each([ int(0) // expired ) ], + pool, { dba: newRoutingTable( 'dba', @@ -3379,53 +3339,98 @@ describe.each([ [server3, server2], [server2, server4] ) - }, - pool - ) + } + ) + connectionProvider._useSeedRouter = false - const auth = { other: 'token' } + const auth = acquireAuth - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) - .catch(functional.identity) + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) - expect(connection._release).toHaveBeenCalled() - }) + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) + expect(connection._release).toHaveBeenCalled() + }) - it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, { some: 'auth' }) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProviderWithSeedRouter( - server0, - [server0], - [], - {}, - pool - ) + it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + { + dba: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + }, + pool + ) - const auth = { other: 'token' } + const auth = acquireAuth - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) - .catch(functional.identity) + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) - expect(connection._release).toHaveBeenCalled() + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + expect(connection._release).toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [], + {}, + pool + ) + + const auth = acquireAuth + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + expect(connection._release).toHaveBeenCalled() + }) }) }) }) From 4e7537bbc63906d78492038242f1ea74027ff6b8 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 1 Feb 2023 19:12:00 +0100 Subject: [PATCH 33/70] Improve tests around non-sticky connections --- .../connection-provider-direct.test.js | 75 +++-- .../connection-provider-routing.test.js | 269 ++++++++++++++---- 2 files changed, 269 insertions(+), 75 deletions(-) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index c79437af0..4e581a2d9 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -615,28 +615,54 @@ describe('.verifyConnectivityAndGetServerInfo()', () => { false, null ])('when allowStickyConnection is %s', (allowStickyConnection) => { - it.each([ - ['new connection', { other: 'auth' }, { other: 'token' }], - ['old connection', { some: 'auth' }, { other: 'token' }] - ])('should raise and error when try switch user on acquire [%s]', async (_, connAuth, acquireAuth) => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newDirectConnectionProvider(address, pool) - - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: '', - allowStickyConnection, - auth: acquireAuth - }) - .catch(functional.identity) + describe('when does not supports re-auth', () => { + it.each([ + ['new connection', { other: 'auth' }, { other: 'token' }], + ['old connection', { some: 'auth' }, { other: 'token' }] + ])('should raise and error when try switch user on acquire [%s]', async (_, connAuth, acquireAuth) => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newDirectConnectionProvider(address, pool) + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth: acquireAuth + }) + .catch(functional.identity) - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address) - expect(connection._release).toHaveBeenCalled() + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address) + expect(connection._release).toHaveBeenCalled() + }) + }) + + describe('when supports re-auth', () => { + const connAuth = { some: 'auth' } + const acquireAuth = connAuth + + it('should raise and error when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newDirectConnectionProvider(address, pool) + + const delegatedConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth: acquireAuth + }) + + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) + }) }) }) }) @@ -669,7 +695,7 @@ function newPool ({ create, config } = {}) { } class FakeConnection extends Connection { - constructor (address, release, server, auth) { + constructor (address, release, server, auth, { supportsReAuth } = {}) { super(null) this._address = address @@ -678,6 +704,7 @@ class FakeConnection extends Connection { this._authToken = auth this._closed = false this._id = 1 + this._supportsReAuth = supportsReAuth || false } get id () { @@ -700,6 +727,10 @@ class FakeConnection extends Connection { return this._server } + get supportsReAuth () { + return this._supportsReAuth + } + async close () { this._closed = true } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 82c11b5f1..2c35364c6 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -1825,7 +1825,7 @@ describe.each([ expect(capturedError.code).toBe(SERVICE_UNAVAILABLE) expect(capturedError.message).toBe( 'Server at server-non-existing-seed-router:7687 can\'t ' + - 'perform routing. Make sure you are connecting to a causal cluster' + 'perform routing. Make sure you are connecting to a causal cluster' ) // Error should be the cause of the given capturedError expect(capturedError).toEqual(newError(capturedError.message, capturedError.code, error)) @@ -2907,7 +2907,7 @@ describe.each([ }) }) - function setup ({ resetAndFlush, releaseMock, newConnection } = { }) { + function setup ({ resetAndFlush, releaseMock, newConnection } = {}) { const routingTable = newRoutingTable( database || null, [server1, server2], @@ -3252,7 +3252,7 @@ describe.each([ const log = new Logger('debug', () => undefined) jest.spyOn(log, 'debug') const createChannelConnectionHook = createConnection || jest.fn(async (address) => new FakeConnection(address)) - const authenticationProviderHook = new AuthenticationProvider({ }) + const authenticationProviderHook = new AuthenticationProvider({}) jest.spyOn(authenticationProviderHook, 'authenticate') .mockImplementation(authenticationProvider || jest.fn(({ connection }) => Promise.resolve(connection))) const provider = new RoutingConnectionProvider({ @@ -3282,15 +3282,168 @@ describe.each([ false, null ])('when allowStickyConnection is %s', (allowStickyConnection) => { - describe.each([ - ['new connection', { other: 'auth' }, { other: 'token' }], - ['old connection', { some: 'auth' }, { other: 'token' }] - ])('%s', (_, connAuth, acquireAuth) => { - it('should raise and error when try switch user on acquire', async () => { + describe('when does not support re-auth', () => { + describe.each([ + ['new connection', { other: 'auth' }, { other: 'token' }], + ['old connection', { some: 'auth' }, { other: 'token' }] + ])('%s', (_, connAuth, acquireAuth) => { + it('should raise and error when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + const auth = acquireAuth + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) + expect(connection._release).toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [expired rt]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + pool, + { + dba: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + ) + connectionProvider._useSeedRouter = false + + const auth = acquireAuth + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) + expect(connection._release).toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + { + dba: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + }, + pool + ) + + const auth = acquireAuth + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + expect(connection._release).toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [], + {}, + pool + ) + + const auth = acquireAuth + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + expect(connection._release).toHaveBeenCalled() + }) + }) + }) + + describe('when does it support re-auth', () => { + const connAuth = { myAuth: 'auth' } + const acquireAuth = connAuth + + it('should return connection when try switch user on acquire', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newRoutingConnectionProvider([ newRoutingTable( null, @@ -3303,25 +3456,23 @@ describe.each([ ) const auth = acquireAuth - const error = await connectionProvider + const delegatedConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: '', allowStickyConnection, auth }) - .catch(functional.identity) - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) - expect(connection._release).toHaveBeenCalled() + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) }) - it('should raise and error when try switch user on acquire [expired rt]', async () => { + it('should return connection when try switch user on acquire [expired rt]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newRoutingConnectionProvider([ newRoutingTable( 'dba', @@ -3333,37 +3484,37 @@ describe.each([ ], pool, { - dba: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) + dba: { + [server1.asHostPort()]: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + } } ) connectionProvider._useSeedRouter = false const auth = acquireAuth - const error = await connectionProvider + const delegatedConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', allowStickyConnection, auth }) - .catch(functional.identity) - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) - expect(connection._release).toHaveBeenCalled() + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) }) - it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { + it('should return delegated connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newRoutingConnectionProviderWithSeedRouter( server0, [server0], @@ -3377,59 +3528,66 @@ describe.each([ ) ], { - dba: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) + dba: { + [server0.asHostPort()]: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + } }, pool ) const auth = acquireAuth - const error = await connectionProvider + const delegatedConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', allowStickyConnection, auth }) - .catch(functional.identity) - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) - expect(connection._release).toHaveBeenCalled() + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) }) - it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { + it('should delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newRoutingConnectionProviderWithSeedRouter( server0, [server0], [], - {}, + { + dba: { + [server0.asHostPort()]: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + }, pool ) const auth = acquireAuth - const error = await connectionProvider + const delegatedConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', allowStickyConnection, auth }) - .catch(functional.identity) - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) - expect(connection._release).toHaveBeenCalled() + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) }) }) }) @@ -3597,7 +3755,7 @@ function expectPoolToNotContain (pool, addresses) { } class FakeConnection extends Connection { - constructor (address, release, version, protocolVersion, server, authToken) { + constructor (address, release, version, protocolVersion, server, authToken, { supportsReAuth } = {}) { super(null) this._address = address @@ -3610,6 +3768,7 @@ class FakeConnection extends Connection { this._authToken = authToken this._id = 1 this._closed = false + this._supportsReAuth = supportsReAuth || false } get id () { @@ -3636,6 +3795,10 @@ class FakeConnection extends Connection { return this._server } + get supportsReAuth () { + return this._supportsReAuth + } + async close () { this._closed = true } From 2b24f1530a203a4aab19a885a760a084359207db Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 2 Feb 2023 11:30:46 +0100 Subject: [PATCH 34/70] Mark connections created with a new auth and without support to re-auth as sticky --- .../connection-provider-pooled.js | 7 +- .../connection-provider-direct.test.js | 578 +++++++++--------- .../connection-provider-routing.test.js | 14 +- 3 files changed, 306 insertions(+), 293 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 59da5dffc..c6134d147 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -171,13 +171,16 @@ export default class PooledConnectionProvider extends ConnectionProvider { async _getStickyConnection ({ auth, connection, allowStickyConnection }) { const connectionWithSameCredentials = object.equals(auth, connection.authToken) const shouldCreateStickyConnection = !connectionWithSameCredentials - const stickyConnectionWasCreated = connectionWithSameCredentials && !connection.supportsReAuth - if (allowStickyConnection !== true && (shouldCreateStickyConnection || stickyConnectionWasCreated)) { + connection._sticky = connectionWithSameCredentials && !connection.supportsReAuth + + if (allowStickyConnection !== true && (shouldCreateStickyConnection || connection._sticky)) { await connection._release() throw newError('Driver is connected to a database that does not support user switch.') } else if (allowStickyConnection === true && shouldCreateStickyConnection) { await connection._release() return await this._createStickyConnection({ address: this._address, auth }) + } else if (connection._sticky) { + return connection } } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 4e581a2d9..9d79ad676 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -206,6 +206,296 @@ it('should call authenticationAuthProvider.handleError when TokenExpired happens expect(handleError).toBeCalledWith({ connection: conn, code: 'Neo.ClientError.Security.TokenExpired' }) }) +describe('constructor', () => { + describe('newPool', () => { + const server0 = ServerAddress.fromUrl('localhost:123') + const server01 = ServerAddress.fromUrl('localhost:1235') + + describe('param.create', () => { + it('should create connection', async () => { + const { create, createChannelConnectionHook, provider } = setup() + + const connection = await create({}, server0, undefined) + + expect(createChannelConnectionHook).toHaveBeenCalledWith(server0) + expect(provider._openConnections[connection.id]).toBe(connection) + await expect(createChannelConnectionHook.mock.results[0].value).resolves.toBe(connection) + }) + + it('should register the release function into the connection', async () => { + const { create } = setup() + const releaseResult = { property: 'some property' } + const release = jest.fn(() => releaseResult) + + const connection = await create({}, server0, release) + + const released = connection._release() + + expect(released).toBe(releaseResult) + expect(release).toHaveBeenCalledWith(server0, connection) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should authenticate connection (auth = %o)', async (auth) => { + const { create, authenticationProviderHook } = setup() + + const connection = await create({ auth }, server0) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, + auth + }) + }) + + it('should handle create connection failures', async () => { + const error = newError('some error') + const createConnection = jest.fn(() => Promise.reject(error)) + const { create, authenticationProviderHook, provider } = setup({ createConnection }) + const openConnections = { ...provider._openConnections } + + await expect(create({}, server0)).rejects.toThrow(error) + + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + expect(provider._openConnections).toEqual(openConnections) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should handle authentication failures (auth = %o)', async (auth) => { + const error = newError('some error') + const authenticationProvider = jest.fn(() => Promise.reject(error)) + const { create, authenticationProviderHook, createChannelConnectionHook, provider } = setup({ authenticationProvider }) + const openConnections = { ...provider._openConnections } + + await expect(create({ auth }, server0)).rejects.toThrow(error) + + const connection = await createChannelConnectionHook.mock.results[0].value + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ auth, connection }) + expect(provider._openConnections).toEqual(openConnections) + expect(connection._closed).toBe(true) + }) + }) + + describe('param.destroy', () => { + it('should close connection and unregister it', async () => { + const { create, destroy, provider } = setup() + const openConnections = { ...provider._openConnections } + const connection = await create({}, server0, undefined) + + await destroy(connection) + + expect(connection._closed).toBe(true) + expect(provider._openConnections).toEqual(openConnections) + }) + }) + + describe('param.validateOnAcquire', () => { + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should return true when connection is open and within the lifetime and authentication succeed (auth=%o)', async (auth) => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + + const { validateOnAcquire, authenticationProviderHook } = setup() + + await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(true) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, auth + }) + }) + + it.each([ + null, + undefined, + { scheme: 'bearer', credentials: 'token01' } + ])('should return true when connection is open and within the lifetime and authentication fails (auth=%o)', async (auth) => { + const connection = new FakeConnection(server0) + const error = newError('failed') + const authenticationProvider = jest.fn(() => Promise.reject(error)) + connection.creationTimestamp = Date.now() + + const { validateOnAcquire, authenticationProviderHook, log } = setup({ authenticationProvider }) + + await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(false) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, auth + }) + + expect(log.debug).toHaveBeenCalledWith( + `The connection ${connection.id} is not valid because of an error ${error.code} '${error.message}'` + ) + }) + it('should return false when connection is closed and within the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + await connection.close() + + const { validateOnAcquire, authenticationProviderHook } = setup() + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + + it('should return false when connection is open and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 }) + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + + it('should return false when connection is closed and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + await connection.close() + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 }) + + await expect(validateOnAcquire({}, connection)).resolves.toBe(false) + expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() + }) + }) + + describe('param.validateOnRelease', () => { + it('should return true when connection is open and within the lifetime', () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + + const { validateOnRelease } = setup() + + expect(validateOnRelease(connection)).toBe(true) + }) + + it('should return false when connection is closed and within the lifetime', async () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() + await connection.close() + + const { validateOnRelease } = setup() + + expect(validateOnRelease(connection)).toBe(false) + }) + + it('should return false when connection is open and out of the lifetime', () => { + const connection = new FakeConnection(server0) + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnRelease } = setup({ maxConnectionLifetime: 3000 }) + + expect(validateOnRelease(connection)).toBe(false) + }) + + it('should return false when connection is closed and out of the lifetime', async () => { + const connection = new FakeConnection(server0) + await connection.close() + connection.creationTimestamp = Date.now() - 4000 + + const { validateOnRelease } = setup({ maxConnectionLifetime: 3000 }) + + expect(validateOnRelease(connection)).toBe(false) + }) + }) + + function setup ({ createConnection, authenticationProvider, maxConnectionLifetime } = {}) { + const newPool = jest.fn((...args) => new Pool(...args)) + const log = new Logger('debug', () => undefined) + jest.spyOn(log, 'debug') + const createChannelConnectionHook = createConnection || jest.fn(async (address) => new FakeConnection(address)) + const authenticationProviderHook = new AuthenticationProvider({ }) + jest.spyOn(authenticationProviderHook, 'authenticate') + .mockImplementation(authenticationProvider || jest.fn(({ connection }) => Promise.resolve(connection))) + const provider = new DirectConnectionProvider({ + newPool, + config: { + maxConnectionLifetime: maxConnectionLifetime || 1000 + }, + address: server01, + log + }) + provider._createChannelConnection = createChannelConnectionHook + provider._authenticationProvider = authenticationProviderHook + return { + provider, + ...newPool.mock.calls[0][0], + createChannelConnectionHook, + authenticationProviderHook, + log + } + } + }) +}) + +describe('user-switching', () => { + describe.each([ + undefined, + false, + null + ])('when allowStickyConnection is %s', (allowStickyConnection) => { + describe('when does not supports re-auth', () => { + it.each([ + ['new connection', { other: 'auth' }, { other: 'auth' }, true], + ['old connection', { some: 'auth' }, { other: 'token' }, false] + ])('should raise and error when try switch user on acquire [%s]', async (_, connAuth, acquireAuth, isStickyConn) => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newDirectConnectionProvider(address, pool) + + const error = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth: acquireAuth + }) + .catch(functional.identity) + + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address) + expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) + }) + }) + + describe('when supports re-auth', () => { + const connAuth = { some: 'auth' } + const acquireAuth = connAuth + + it('should return connection when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newDirectConnectionProvider(address, pool) + + const delegatedConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth: acquireAuth + }) + + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) + }) + }) + }) +}) + describe('.verifyConnectivityAndGetServerInfo()', () => { describe('when connection is available in the pool', () => { it('should return the server info', async () => { @@ -378,294 +668,6 @@ describe('.verifyConnectivityAndGetServerInfo()', () => { } }) }) - - describe('constructor', () => { - describe('newPool', () => { - const server0 = ServerAddress.fromUrl('localhost:123') - const server01 = ServerAddress.fromUrl('localhost:1235') - - describe('param.create', () => { - it('should create connection', async () => { - const { create, createChannelConnectionHook, provider } = setup() - - const connection = await create({}, server0, undefined) - - expect(createChannelConnectionHook).toHaveBeenCalledWith(server0) - expect(provider._openConnections[connection.id]).toBe(connection) - await expect(createChannelConnectionHook.mock.results[0].value).resolves.toBe(connection) - }) - - it('should register the release function into the connection', async () => { - const { create } = setup() - const releaseResult = { property: 'some property' } - const release = jest.fn(() => releaseResult) - - const connection = await create({}, server0, release) - - const released = connection._release() - - expect(released).toBe(releaseResult) - expect(release).toHaveBeenCalledWith(server0, connection) - }) - - it.each([ - null, - undefined, - { scheme: 'bearer', credentials: 'token01' } - ])('should authenticate connection (auth = %o)', async (auth) => { - const { create, authenticationProviderHook } = setup() - - const connection = await create({ auth }, server0) - - expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ - connection, - auth - }) - }) - - it('should handle create connection failures', async () => { - const error = newError('some error') - const createConnection = jest.fn(() => Promise.reject(error)) - const { create, authenticationProviderHook, provider } = setup({ createConnection }) - const openConnections = { ...provider._openConnections } - - await expect(create({}, server0)).rejects.toThrow(error) - - expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() - expect(provider._openConnections).toEqual(openConnections) - }) - - it.each([ - null, - undefined, - { scheme: 'bearer', credentials: 'token01' } - ])('should handle authentication failures (auth = %o)', async (auth) => { - const error = newError('some error') - const authenticationProvider = jest.fn(() => Promise.reject(error)) - const { create, authenticationProviderHook, createChannelConnectionHook, provider } = setup({ authenticationProvider }) - const openConnections = { ...provider._openConnections } - - await expect(create({ auth }, server0)).rejects.toThrow(error) - - const connection = await createChannelConnectionHook.mock.results[0].value - expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ auth, connection }) - expect(provider._openConnections).toEqual(openConnections) - expect(connection._closed).toBe(true) - }) - }) - - describe('param.destroy', () => { - it('should close connection and unregister it', async () => { - const { create, destroy, provider } = setup() - const openConnections = { ...provider._openConnections } - const connection = await create({}, server0, undefined) - - await destroy(connection) - - expect(connection._closed).toBe(true) - expect(provider._openConnections).toEqual(openConnections) - }) - }) - - describe('param.validateOnAcquire', () => { - it.each([ - null, - undefined, - { scheme: 'bearer', credentials: 'token01' } - ])('should return true when connection is open and within the lifetime and authentication succeed (auth=%o)', async (auth) => { - const connection = new FakeConnection(server0) - connection.creationTimestamp = Date.now() - - const { validateOnAcquire, authenticationProviderHook } = setup() - - await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(true) - - expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ - connection, auth - }) - }) - - it.each([ - null, - undefined, - { scheme: 'bearer', credentials: 'token01' } - ])('should return true when connection is open and within the lifetime and authentication fails (auth=%o)', async (auth) => { - const connection = new FakeConnection(server0) - const error = newError('failed') - const authenticationProvider = jest.fn(() => Promise.reject(error)) - connection.creationTimestamp = Date.now() - - const { validateOnAcquire, authenticationProviderHook, log } = setup({ authenticationProvider }) - - await expect(validateOnAcquire({ auth }, connection)).resolves.toBe(false) - - expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ - connection, auth - }) - - expect(log.debug).toHaveBeenCalledWith( - `The connection ${connection.id} is not valid because of an error ${error.code} '${error.message}'` - ) - }) - it('should return false when connection is closed and within the lifetime', async () => { - const connection = new FakeConnection(server0) - connection.creationTimestamp = Date.now() - await connection.close() - - const { validateOnAcquire, authenticationProviderHook } = setup() - - await expect(validateOnAcquire({}, connection)).resolves.toBe(false) - expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() - }) - - it('should return false when connection is open and out of the lifetime', async () => { - const connection = new FakeConnection(server0) - connection.creationTimestamp = Date.now() - 4000 - - const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 }) - - await expect(validateOnAcquire({}, connection)).resolves.toBe(false) - expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() - }) - - it('should return false when connection is closed and out of the lifetime', async () => { - const connection = new FakeConnection(server0) - await connection.close() - connection.creationTimestamp = Date.now() - 4000 - - const { validateOnAcquire, authenticationProviderHook } = setup({ maxConnectionLifetime: 3000 }) - - await expect(validateOnAcquire({}, connection)).resolves.toBe(false) - expect(authenticationProviderHook.authenticate).not.toHaveBeenCalled() - }) - }) - - describe('param.validateOnRelease', () => { - it('should return true when connection is open and within the lifetime', () => { - const connection = new FakeConnection(server0) - connection.creationTimestamp = Date.now() - - const { validateOnRelease } = setup() - - expect(validateOnRelease(connection)).toBe(true) - }) - - it('should return false when connection is closed and within the lifetime', async () => { - const connection = new FakeConnection(server0) - connection.creationTimestamp = Date.now() - await connection.close() - - const { validateOnRelease } = setup() - - expect(validateOnRelease(connection)).toBe(false) - }) - - it('should return false when connection is open and out of the lifetime', () => { - const connection = new FakeConnection(server0) - connection.creationTimestamp = Date.now() - 4000 - - const { validateOnRelease } = setup({ maxConnectionLifetime: 3000 }) - - expect(validateOnRelease(connection)).toBe(false) - }) - - it('should return false when connection is closed and out of the lifetime', async () => { - const connection = new FakeConnection(server0) - await connection.close() - connection.creationTimestamp = Date.now() - 4000 - - const { validateOnRelease } = setup({ maxConnectionLifetime: 3000 }) - - expect(validateOnRelease(connection)).toBe(false) - }) - }) - - function setup ({ createConnection, authenticationProvider, maxConnectionLifetime } = {}) { - const newPool = jest.fn((...args) => new Pool(...args)) - const log = new Logger('debug', () => undefined) - jest.spyOn(log, 'debug') - const createChannelConnectionHook = createConnection || jest.fn(async (address) => new FakeConnection(address)) - const authenticationProviderHook = new AuthenticationProvider({ }) - jest.spyOn(authenticationProviderHook, 'authenticate') - .mockImplementation(authenticationProvider || jest.fn(({ connection }) => Promise.resolve(connection))) - const provider = new DirectConnectionProvider({ - newPool, - config: { - maxConnectionLifetime: maxConnectionLifetime || 1000 - }, - address: server01, - log - }) - provider._createChannelConnection = createChannelConnectionHook - provider._authenticationProvider = authenticationProviderHook - return { - provider, - ...newPool.mock.calls[0][0], - createChannelConnectionHook, - authenticationProviderHook, - log - } - } - }) - }) - - describe('user-switching', () => { - describe.each([ - undefined, - false, - null - ])('when allowStickyConnection is %s', (allowStickyConnection) => { - describe('when does not supports re-auth', () => { - it.each([ - ['new connection', { other: 'auth' }, { other: 'token' }], - ['old connection', { some: 'auth' }, { other: 'token' }] - ])('should raise and error when try switch user on acquire [%s]', async (_, connAuth, acquireAuth) => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newDirectConnectionProvider(address, pool) - - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: '', - allowStickyConnection, - auth: acquireAuth - }) - .catch(functional.identity) - - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address) - expect(connection._release).toHaveBeenCalled() - }) - }) - - describe('when supports re-auth', () => { - const connAuth = { some: 'auth' } - const acquireAuth = connAuth - - it('should raise and error when try switch user on acquire', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newDirectConnectionProvider(address, pool) - - const delegatedConnection = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: '', - allowStickyConnection, - auth: acquireAuth - }) - - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - }) - }) - }) - }) }) function newDirectConnectionProvider (address, pool) { diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 2c35364c6..c235231dc 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3284,9 +3284,9 @@ describe.each([ ])('when allowStickyConnection is %s', (allowStickyConnection) => { describe('when does not support re-auth', () => { describe.each([ - ['new connection', { other: 'auth' }, { other: 'token' }], - ['old connection', { some: 'auth' }, { other: 'token' }] - ])('%s', (_, connAuth, acquireAuth) => { + ['new connection', { other: 'auth' }, { other: 'auth' }, true], + ['old connection', { some: 'auth' }, { other: 'token' }, false] + ])('%s', (_, connAuth, acquireAuth, isStickyConn) => { it('should raise and error when try switch user on acquire', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() @@ -3316,6 +3316,7 @@ describe.each([ expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) }) it('should raise and error when try switch user on acquire [expired rt]', async () => { @@ -3358,6 +3359,7 @@ describe.each([ expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) }) it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { @@ -3402,6 +3404,7 @@ describe.each([ expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) }) it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { @@ -3431,6 +3434,7 @@ describe.each([ expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) }) }) }) @@ -3466,6 +3470,7 @@ describe.each([ expect(delegatedConnection).toBeInstanceOf(DelegateConnection) expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) }) it('should return connection when try switch user on acquire [expired rt]', async () => { @@ -3508,6 +3513,7 @@ describe.each([ expect(delegatedConnection).toBeInstanceOf(DelegateConnection) expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) }) it('should return delegated connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { @@ -3552,6 +3558,7 @@ describe.each([ expect(delegatedConnection).toBeInstanceOf(DelegateConnection) expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) }) it('should delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { @@ -3588,6 +3595,7 @@ describe.each([ expect(delegatedConnection).toBeInstanceOf(DelegateConnection) expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) }) }) }) From 31447bee842010a7930cd737b5015a748bc0f82b Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 2 Feb 2023 11:35:10 +0100 Subject: [PATCH 35/70] Killing sticky connections on-release --- .../connection-provider/connection-provider-pooled.js | 2 +- .../connection-provider-direct.test.js | 9 +++++++++ .../connection-provider-routing.test.js | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index c6134d147..07eea95a8 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -104,7 +104,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { } _validateConnectionOnRelease (conn) { - return this._validateConnection(conn) + return conn._sticky !== true && this._validateConnection(conn) } /** diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 9d79ad676..d25b943fd 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -405,6 +405,15 @@ describe('constructor', () => { expect(validateOnRelease(connection)).toBe(false) }) + + it('should return false when connection is sticky', async () => { + const connection = new FakeConnection(server0) + connection._sticky = true + + const { validateOnRelease } = setup() + + expect(validateOnRelease(connection)).toBe(false) + }) }) function setup ({ createConnection, authenticationProvider, maxConnectionLifetime } = {}) { diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index c235231dc..dd46c273a 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3245,6 +3245,15 @@ describe.each([ expect(validateOnRelease(connection)).toBe(false) }) + + it('should return false when connection is sticky', async () => { + const connection = new FakeConnection(server0) + connection._sticky = true + + const { validateOnRelease } = setup() + + expect(validateOnRelease(connection)).toBe(false) + }) }) function setup ({ createConnection, authenticationProvider, maxConnectionLifetime } = {}) { From ec560baa1cb396bf1542659cd97a15d14cb3ecdc Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 2 Feb 2023 12:57:05 +0100 Subject: [PATCH 36/70] Enable require a new connection from the pool --- packages/bolt-connection/src/pool/pool.js | 56 +++-- .../bolt-connection/test/pool/pool.test.js | 198 ++++++++++++++++++ .../connection-provider-pooled.js | 9 +- .../lib/bolt-connection/pool/pool.js | 56 +++-- 4 files changed, 278 insertions(+), 41 deletions(-) diff --git a/packages/bolt-connection/src/pool/pool.js b/packages/bolt-connection/src/pool/pool.js index bba636ace..41398a281 100644 --- a/packages/bolt-connection/src/pool/pool.js +++ b/packages/bolt-connection/src/pool/pool.js @@ -76,9 +76,11 @@ class Pool { * Acquire and idle resource fom the pool or create a new one. * @param {object} acquisitionContext the acquisition context used for create and validateOnAcquire connection * @param {ServerAddress} address the address for which we're acquiring. + * @param {object} config the config + * @param {boolean} config.requireNew Indicate it requires a new resource * @return {Promise} resource that is ready to use. */ - acquire (acquisitionContext, address) { + acquire (acquisitionContext, address, config) { const key = address.asKey() // We're out of resources and will try to acquire later on when an existing resource is released. @@ -114,7 +116,7 @@ class Pool { } }, this._acquisitionTimeout) - request = new PendingRequest(key, acquisitionContext, resolve, reject, timeoutId, this._log) + request = new PendingRequest(key, acquisitionContext, config, resolve, reject, timeoutId, this._log) allRequests[key].push(request) this._processPendingAcquireRequests(address) }) @@ -199,30 +201,32 @@ class Pool { return pool } - async _acquire (acquisitionContext, address) { + async _acquire (acquisitionContext, address, requireNew) { if (this._closed) { throw newError('Pool is closed, it is no more able to serve requests.') } const key = address.asKey() const pool = this._getOrInitializePoolFor(key) - while (pool.length) { - const resource = pool.pop() + if (!requireNew) { + while (pool.length) { + const resource = pool.pop() - if (this._removeIdleObserver) { - this._removeIdleObserver(resource) - } + if (this._removeIdleObserver) { + this._removeIdleObserver(resource) + } - if (await this._validateOnAcquire(acquisitionContext, resource)) { - // idle resource is valid and can be acquired - resourceAcquired(key, this._activeResourceCounts) - if (this._log.isDebugEnabled()) { - this._log.debug(`${resource} acquired from the pool ${key}`) + if (await this._validateOnAcquire(acquisitionContext, resource)) { + // idle resource is valid and can be acquired + resourceAcquired(key, this._activeResourceCounts) + if (this._log.isDebugEnabled()) { + this._log.debug(`${resource} acquired from the pool ${key}`) + } + return { resource, pool } + } else { + pool.removeInUse(resource) + await this._destroy(resource) } - return { resource, pool } - } else { - pool.removeInUse(resource) - await this._destroy(resource) } } @@ -243,6 +247,15 @@ class Pool { this._pendingCreates[key] = this._pendingCreates[key] + 1 let resource try { + if (pool.length && requireNew) { + const resource = pool.pop() + if (this._removeIdleObserver) { + this._removeIdleObserver(resource) + } + pool.removeInUse(resource) + await this._destroy(resource) + } + // Invoke callback that creates actual connection resource = await this._create(acquisitionContext, address, (address, resource) => this._release(address, resource, pool)) pool.pushInUse(resource) @@ -332,7 +345,7 @@ class Pool { const pendingRequest = requests.shift() // pop a pending acquire request if (pendingRequest) { - this._acquire(pendingRequest.context, address) + this._acquire(pendingRequest.context, address, pendingRequest.requireNew) .catch(error => { // failed to acquire/create a new connection to resolve the pending acquire request // propagate the error by failing the pending request @@ -396,7 +409,7 @@ function resourceReleased (key, activeResourceCounts) { } class PendingRequest { - constructor (key, context, resolve, reject, timeoutId, log) { + constructor (key, context, config, resolve, reject, timeoutId, log) { this._key = key this._context = context this._resolve = resolve @@ -404,12 +417,17 @@ class PendingRequest { this._timeoutId = timeoutId this._log = log this._completed = false + this._config = config || {} } get context () { return this._context } + get requireNew () { + return this._config.requireNew || false + } + isCompleted () { return this._completed } diff --git a/packages/bolt-connection/test/pool/pool.test.js b/packages/bolt-connection/test/pool/pool.test.js index 2aa80c644..b224f9512 100644 --- a/packages/bolt-connection/test/pool/pool.test.js +++ b/packages/bolt-connection/test/pool/pool.test.js @@ -467,6 +467,7 @@ describe('#unit Pool', () => { message: expect.stringMatching('Pool is closed') }) }) + it('purges keys other than the ones to keep', async () => { let counter = 0 @@ -1063,6 +1064,196 @@ describe('#unit Pool', () => { resource2, resource1 ]) }) + + describe('when acquire force new', () => { + it('allocates if pool is empty', async () => { + // Given + let counter = 0 + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_, server, release) => + Promise.resolve(new Resource(server, counter++, release)) + }) + + // When + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address, { requireNew: true }) + + // Then + expect(r0.id).toBe(0) + expect(r1.id).toBe(1) + expect(r0).not.toBe(r1) + }) + + it('not pools if resources are returned', async () => { + // Given a pool that allocates + let counter = 0 + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_, server, release) => + Promise.resolve(new Resource(server, counter++, release)) + }) + + // When + const r0 = await pool.acquire({}, address) + await r0.close() + + const r1 = await pool.acquire({}, address, { requireNew: true }) + + // Then + expect(r0.id).toBe(0) + expect(r1.id).toBe(1) + expect(r0).not.toBe(r1) + }) + + it('should fail to acquire when closed', async () => { + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_, server, release) => + Promise.resolve(new Resource(server, 0, release)), + destroy: res => { + return Promise.resolve() + } + }) + + // Close the pool + await pool.close() + + await expect(pool.acquire({}, address, { requireNew: true })).rejects.toMatchObject({ + message: expect.stringMatching('Pool is closed') + }) + }) + + it('should fail to acquire when closed with idle connections', async () => { + const address = ServerAddress.fromUrl('bolt://localhost:7687') + + const pool = new Pool({ + create: (_, server, release) => + Promise.resolve(new Resource(server, 0, release)), + destroy: res => { + return Promise.resolve() + } + }) + + // Acquire and release a resource + const resource = await pool.acquire({}, address) + await resource.close() + + // Close the pool + await pool.close() + + await expect(pool.acquire({}, address, { requireNew: true })).rejects.toMatchObject({ + message: expect.stringMatching('Pool is closed') + }) + }) + + it('should wait for a returned connection when max pool size is reached', async () => { + let counter = 0 + + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_, server, release) => + Promise.resolve(new Resource(server, counter++, release)), + destroy: res => Promise.resolve(), + config: new PoolConfig(2, 5000) + }) + + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address, { requireNew: true }) + + setTimeout(() => { + expectNumberOfAcquisitionRequests(pool, address, 1) + r1.close() + }, 1000) + + expect(r1).not.toBe(r0) + const r2 = await pool.acquire({}, address) + expect(r2).toBe(r1) + }) + + it('should wait for a returned connection when max pool size is reached and return new', async () => { + let counter = 0 + + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_, server, release) => + Promise.resolve(new Resource(server, counter++, release)), + destroy: res => Promise.resolve(), + config: new PoolConfig(2, 5000) + }) + + const r0 = await pool.acquire({}, address) + const r1 = await pool.acquire({}, address, { requireNew: true }) + + setTimeout(() => { + expectNumberOfAcquisitionRequests(pool, address, 1) + r1.close() + }, 1000) + + expect(r1).not.toBe(r0) + const r2 = await pool.acquire({}, address, { requireNew: true }) + expect(r2).not.toBe(r1) + }) + + it('should handle a sequence of request new and the regular request', async () => { + let counter = 0 + + const destroy = jest.fn(res => Promise.resolve()) + const removeIdleObserver = jest.fn(res => undefined) + const address = ServerAddress.fromUrl('bolt://localhost:7687') + const pool = new Pool({ + create: (_, server, release) => + Promise.resolve(new Resource(server, counter++, release)), + destroy: destroy, + removeIdleObserver: removeIdleObserver, + config: new PoolConfig(1, 5000) + }) + + const r0 = await pool.acquire({}, address, { requireNew: true }) + expect(pool.activeResourceCount(address)).toEqual(1) + expect(idleResources(pool, address)).toBe(0) + expect(resourceInUse(pool, address)).toBe(1) + + setTimeout(() => { + expectNumberOfAcquisitionRequests(pool, address, 1) + r0.close() + }, 1000) + + const r1 = await pool.acquire({}, address, { requireNew: true }) + expect(destroy).toHaveBeenCalledWith(r0) + expect(removeIdleObserver).toHaveBeenCalledWith(r0) + expect(pool.activeResourceCount(address)).toEqual(1) + expect(idleResources(pool, address)).toBe(0) + expect(resourceInUse(pool, address)).toBe(1) + + setTimeout(() => { + expectNumberOfAcquisitionRequests(pool, address, 1) + r1.close() + }, 1000) + + expect(r1).not.toBe(r0) + const r2 = await pool.acquire({}, address, { requireNew: true }) + expect(removeIdleObserver).toHaveBeenCalledWith(r1) + expect(destroy).toHaveBeenCalledWith(r1) + expect(r2).not.toBe(r1) + expect(pool.activeResourceCount(address)).toEqual(1) + expect(idleResources(pool, address)).toBe(0) + expect(resourceInUse(pool, address)).toBe(1) + + setTimeout(() => { + expectNumberOfAcquisitionRequests(pool, address, 1) + r2.close() + }, 1000) + + const r3 = await pool.acquire({}, address) + expect(r3).toBe(r2) + expect(removeIdleObserver).toHaveBeenCalledWith(r2) + expect(destroy).not.toHaveBeenCalledWith(r2) + expect(pool.activeResourceCount(address)).toEqual(1) + expect(idleResources(pool, address)).toBe(0) + expect(resourceInUse(pool, address)).toBe(1) + }) + }) }) function expectNoPendingAcquisitionRequests (pool) { @@ -1088,6 +1279,13 @@ function idleResources (pool, address) { return undefined } +function resourceInUse (pool, address) { + if (pool.has(address)) { + return pool._pools[address.asKey()]._elementsInUse.size + } + return undefined +} + function expectNumberOfAcquisitionRequests (pool, address, expectedNumber) { expect(pool._acquireRequests[address.asKey()].length).toEqual(expectedNumber) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 2017fd5bc..cf4d4f5e0 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -104,7 +104,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { } _validateConnectionOnRelease (conn) { - return this._validateConnection(conn) + return conn._sticky !== true && this._validateConnection(conn) } /** @@ -171,13 +171,16 @@ export default class PooledConnectionProvider extends ConnectionProvider { async _getStickyConnection ({ auth, connection, allowStickyConnection }) { const connectionWithSameCredentials = object.equals(auth, connection.authToken) const shouldCreateStickyConnection = !connectionWithSameCredentials - const stickyConnectionWasCreated = connectionWithSameCredentials && !connection.supportsReAuth - if (allowStickyConnection !== true && (shouldCreateStickyConnection || stickyConnectionWasCreated)) { + connection._sticky = connectionWithSameCredentials && !connection.supportsReAuth + + if (allowStickyConnection !== true && (shouldCreateStickyConnection || connection._sticky)) { await connection._release() throw newError('Driver is connected to a database that does not support user switch.') } else if (allowStickyConnection === true && shouldCreateStickyConnection) { await connection._release() return await this._createStickyConnection({ address: this._address, auth }) + } else if (connection._sticky) { + return connection } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js index c0451efd3..c5fe6c769 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js @@ -76,9 +76,11 @@ class Pool { * Acquire and idle resource fom the pool or create a new one. * @param {object} acquisitionContext the acquisition context used for create and validateOnAcquire connection * @param {ServerAddress} address the address for which we're acquiring. + * @param {object} config the config + * @param {boolean} config.requireNew Indicate it requires a new resource * @return {Promise} resource that is ready to use. */ - acquire (acquisitionContext, address) { + acquire (acquisitionContext, address, config) { const key = address.asKey() // We're out of resources and will try to acquire later on when an existing resource is released. @@ -114,7 +116,7 @@ class Pool { } }, this._acquisitionTimeout) - request = new PendingRequest(key, acquisitionContext, resolve, reject, timeoutId, this._log) + request = new PendingRequest(key, acquisitionContext, config, resolve, reject, timeoutId, this._log) allRequests[key].push(request) this._processPendingAcquireRequests(address) }) @@ -199,30 +201,32 @@ class Pool { return pool } - async _acquire (acquisitionContext, address) { + async _acquire (acquisitionContext, address, requireNew) { if (this._closed) { throw newError('Pool is closed, it is no more able to serve requests.') } const key = address.asKey() const pool = this._getOrInitializePoolFor(key) - while (pool.length) { - const resource = pool.pop() + if (!requireNew) { + while (pool.length) { + const resource = pool.pop() - if (this._removeIdleObserver) { - this._removeIdleObserver(resource) - } + if (this._removeIdleObserver) { + this._removeIdleObserver(resource) + } - if (await this._validateOnAcquire(acquisitionContext, resource)) { - // idle resource is valid and can be acquired - resourceAcquired(key, this._activeResourceCounts) - if (this._log.isDebugEnabled()) { - this._log.debug(`${resource} acquired from the pool ${key}`) + if (await this._validateOnAcquire(acquisitionContext, resource)) { + // idle resource is valid and can be acquired + resourceAcquired(key, this._activeResourceCounts) + if (this._log.isDebugEnabled()) { + this._log.debug(`${resource} acquired from the pool ${key}`) + } + return { resource, pool } + } else { + pool.removeInUse(resource) + await this._destroy(resource) } - return { resource, pool } - } else { - pool.removeInUse(resource) - await this._destroy(resource) } } @@ -243,6 +247,15 @@ class Pool { this._pendingCreates[key] = this._pendingCreates[key] + 1 let resource try { + if (pool.length && requireNew) { + const resource = pool.pop() + if (this._removeIdleObserver) { + this._removeIdleObserver(resource) + } + pool.removeInUse(resource) + await this._destroy(resource) + } + // Invoke callback that creates actual connection resource = await this._create(acquisitionContext, address, (address, resource) => this._release(address, resource, pool)) pool.pushInUse(resource) @@ -332,7 +345,7 @@ class Pool { const pendingRequest = requests.shift() // pop a pending acquire request if (pendingRequest) { - this._acquire(pendingRequest.context, address) + this._acquire(pendingRequest.context, address, pendingRequest.requireNew) .catch(error => { // failed to acquire/create a new connection to resolve the pending acquire request // propagate the error by failing the pending request @@ -396,7 +409,7 @@ function resourceReleased (key, activeResourceCounts) { } class PendingRequest { - constructor (key, context, resolve, reject, timeoutId, log) { + constructor (key, context, config, resolve, reject, timeoutId, log) { this._key = key this._context = context this._resolve = resolve @@ -404,12 +417,17 @@ class PendingRequest { this._timeoutId = timeoutId this._log = log this._completed = false + this._config = config || {} } get context () { return this._context } + get requireNew () { + return this._config.requireNew || false + } + isCompleted () { return this._completed } From 00287ec83a1ed34153a823309f888320457b832f Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 2 Feb 2023 16:49:12 +0100 Subject: [PATCH 37/70] Acquire new sticky connection if backwards compatible and protocol doesn't support re-auth --- .../connection-provider-direct.js | 7 +- .../connection-provider-pooled.js | 19 +- .../connection-provider-routing.js | 25 +- .../connection-provider-direct.test.js | 67 +++ .../connection-provider-routing.test.js | 410 +++++++++++++++++- .../connection-provider-direct.js | 7 +- .../connection-provider-pooled.js | 19 +- .../connection-provider-routing.js | 25 +- 8 files changed, 534 insertions(+), 45 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 4191aa14b..04a88076f 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -52,7 +52,12 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { const connection = await this._connectionPool.acquire({ auth }, this._address) if (auth) { - const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + const stickyConnection = await this._getStickyConnection({ + auth, + connection, + address: this._address, + allowStickyConnection + }) if (stickyConnection) { return stickyConnection } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 07eea95a8..974330c7c 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -126,19 +126,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { return true } - async _createStickyConnection ({ address, auth }) { - const connection = await this._createChannelConnection(address) - connection._release = () => this._destroyConnection(connection) - this._openConnections[connection.id] = connection - - try { - return await connection.connect(this._userAgent, auth) - } catch (error) { - await this._destroyConnection(connection) - throw error - } - } - /** * Dispose of a connection. * @return {Connection} the connection to dispose. @@ -168,7 +155,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { return serverInfo } - async _getStickyConnection ({ auth, connection, allowStickyConnection }) { + async _getStickyConnection ({ auth, connection, address, allowStickyConnection }) { const connectionWithSameCredentials = object.equals(auth, connection.authToken) const shouldCreateStickyConnection = !connectionWithSameCredentials connection._sticky = connectionWithSameCredentials && !connection.supportsReAuth @@ -178,7 +165,9 @@ export default class PooledConnectionProvider extends ConnectionProvider { throw newError('Driver is connected to a database that does not support user switch.') } else if (allowStickyConnection === true && shouldCreateStickyConnection) { await connection._release() - return await this._createStickyConnection({ address: this._address, auth }) + connection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) + connection._sticky = true + return connection } else if (connection._sticky) { return connection } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 011cb4673..c7c3b3ed1 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -194,7 +194,12 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const connection = await this._connectionPool.acquire({ auth }, address) if (auth) { - const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + const stickyConnection = await this._getStickyConnection({ + auth, + connection, + address, + allowStickyConnection + }) if (stickyConnection) { return stickyConnection } @@ -553,12 +558,17 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth, allowStickyConnection) { try { - const connection = await this._connectionPool.acquire({ auth }, routerAddress) + let connection = await this._connectionPool.acquire({ auth }, routerAddress) if (auth) { - const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + const stickyConnection = await this._getStickyConnection({ + auth, + connection, + address: routerAddress, + allowStickyConnection + }) if (stickyConnection) { - return stickyConnection + connection = stickyConnection } } @@ -567,8 +577,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn) }) - const connectionProvider = new SingleConnectionProvider( - new DelegateConnection(connection, databaseSpecificErrorHandler)) + const delegateConnection = !connection._sticky + ? new DelegateConnection(connection, databaseSpecificErrorHandler) + : new DelegateConnection(connection) + + const connectionProvider = new SingleConnectionProvider(delegateConnection) const protocolVersion = connection.protocol().version if (protocolVersion < 4.0) { diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index d25b943fd..41354cb19 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -503,6 +503,73 @@ describe('user-switching', () => { }) }) }) + + describe.each([ + true + ])('when allowStickyConnection is %s', (allowStickyConnection) => { + describe('when does not supports re-auth', () => { + it.each([ + ['new connection', { other: 'auth' }, { other: 'auth' }, false], + ['old connection', { some: 'auth' }, { other: 'token' }, true] + ])('should raise and error when try switch user on acquire [%s]', async (_, connAuth, acquireAuth, shouldCreateNew) => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth) + const connection2 = new FakeConnection(address, () => {}, undefined, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire') + .mockResolvedValueOnce(connection) + .mockResolvedValueOnce(connection2) + const connectionProvider = newDirectConnectionProvider(address, pool) + + const acquiredConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth: acquireAuth + }) + + expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address) + + if (shouldCreateNew) { + expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toBe(false) + expect(acquiredConnection).toEqual(connection2) + expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address, { requireNew: true }) + } else { + expect(acquiredConnection).toEqual(connection) + } + + expect(acquiredConnection._release).not.toHaveBeenCalled() + expect(acquiredConnection._sticky).toEqual(true) + }) + }) + + describe('when supports re-auth', () => { + const connAuth = { some: 'auth' } + const acquireAuth = connAuth + + it('should return connection when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => {}, undefined, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newDirectConnectionProvider(address, pool) + + const delegatedConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth: acquireAuth + }) + + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) + }) + }) + }) }) describe('.verifyConnectivityAndGetServerInfo()', () => { diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index dd46c273a..173ff31ef 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3608,6 +3608,414 @@ describe.each([ }) }) }) + + describe.each([ + true + ])('when allowStickyConnection is %s', (allowStickyConnection) => { + describe('when does not support re-auth', () => { + describe.each([ + ['new connection', { other: 'auth' }, { other: 'auth' }, false], + ['old connection', { some: 'auth' }, { other: 'token' }, true] + ])('%s', (_, connAuth, acquireAuth, shouldCreateNew) => { + it('should raise and error when try switch user on acquire', async () => { + const pool = newPool() + const createdConnections = {} + const poolAcquire = jest.spyOn(pool, 'acquire') + .mockImplementation((_, address) => { + const conn = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + createdConnections[address.asKey()] = [...createdConnections[address.asKey()] || [], conn] + return conn + }) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + const auth = acquireAuth + + const acquiredConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) + const connections = createdConnections[server3.asKey()] + if (shouldCreateNew) { + // discarded connection should not be marked as sticky and release + expect(connections[0]._release).toHaveBeenCalled() + expect(connections[0]._sticky).toBe(false) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3, { requireNew: true }) + } else { + expect(poolAcquire).not.toHaveBeenCalledWith({ auth }, server3, { requireNew: true }) + } + + // used connection should be marked as sticky + expect(connections[connections.length - 1]._release).not.toHaveBeenCalled() + expect(connections[connections.length - 1]._sticky).toBe(true) + + expect(acquiredConnection).toBe(connections[connections.length - 1]) + expect(acquiredConnection._release).not.toHaveBeenCalled() + expect(acquiredConnection._sticky).toEqual(true) + }) + + it('should raise and error when try switch user on acquire [expired rt]', async () => { + const pool = newPool() + const createdConnections = {} + const database = 'dba' + const poolAcquire = jest.spyOn(pool, 'acquire') + .mockImplementation((_, address) => { + const conn = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + createdConnections[address.asKey()] = [...createdConnections[address.asKey()] || [], conn] + return conn + }) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + database, + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + pool, + { + [database]: { + [server1.asKey()]: newRoutingTable( + database, + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + }) + connectionProvider._useSeedRouter = false + + const auth = acquireAuth + + await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database, + allowStickyConnection, + auth + }) + + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) + const connections = createdConnections[server1.asKey()] + if (shouldCreateNew) { + // discarded connection should not be marked as sticky and release + expect(connections[0]._release).toHaveBeenCalled() + expect(connections[0]._sticky).toBe(false) + expect(connections.length).toBe(2) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1, { requireNew: true }) + } else { + expect(connections.length).toBe(1) + expect(poolAcquire).not.toHaveBeenCalledWith({ auth }, server1, { requireNew: true }) + } + + // used connection should be marked as sticky and released + expect(connections[connections.length - 1]._sticky).toBe(true) + // the release will be done by the session.close(), + // since the rediscovery is mocked, the session doesn't acquire the connection. + // thus, never release the connection. + expect(connections[connections.length - 1]._release).not.toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { + const pool = newPool() + const createdConnections = {} + const database = 'dba' + const poolAcquire = jest.spyOn(pool, 'acquire') + .mockImplementation((_, address) => { + const conn = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + createdConnections[address.asKey()] = [...createdConnections[address.asKey()] || [], conn] + return conn + }) + + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [ + newRoutingTable( + database, + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + { + [database]: { + [server0.asKey()]: newRoutingTable( + database, + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + }, + pool + ) + + const auth = acquireAuth + + await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database, + allowStickyConnection, + auth + }) + + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + const connections = createdConnections[server0.asKey()] + if (shouldCreateNew) { + // discarded connection should not be marked as sticky and release + expect(connections[0]._release).toHaveBeenCalled() + expect(connections[0]._sticky).toBe(false) + expect(connections.length).toBe(2) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0, { requireNew: true }) + } else { + expect(connections.length).toBe(1) + expect(poolAcquire).not.toHaveBeenCalledWith({ auth }, server0, { requireNew: true }) + } + + // used connection should be marked as sticky and released + expect(connections[connections.length - 1]._sticky).toBe(true) + // the release will be done by the session.close(), + // since the rediscovery is mocked, the session doesn't acquire the connection. + // thus, never release the connection. + expect(connections[connections.length - 1]._release).not.toHaveBeenCalled() + }) + + it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { + const pool = newPool() + const createdConnections = {} + const database = 'dba' + const poolAcquire = jest.spyOn(pool, 'acquire') + .mockImplementation((_, address) => { + const conn = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + createdConnections[address.asKey()] = [...createdConnections[address.asKey()] || [], conn] + return conn + }) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [], + { + [database]: { + [server0.asKey()]: newRoutingTable( + database, + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + }, + pool + ) + + const auth = acquireAuth + + await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database, + allowStickyConnection, + auth + }) + + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + const connections = createdConnections[server0.asKey()] + if (shouldCreateNew) { + // discarded connection should not be marked as sticky and release + expect(connections[0]._release).toHaveBeenCalled() + expect(connections[0]._sticky).toBe(false) + expect(connections.length).toBe(2) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0, { requireNew: true }) + } else { + expect(connections.length).toBe(1) + expect(poolAcquire).not.toHaveBeenCalledWith({ auth }, server0, { requireNew: true }) + } + + // used connection should be marked as sticky and released + expect(connections[connections.length - 1]._sticky).toBe(true) + // the release will be done by the session.close(), + // since the rediscovery is mocked, the session doesn't acquire the connection. + // thus, never release the connection. + expect(connections[connections.length - 1]._release).not.toHaveBeenCalled() + }) + }) + }) + + describe('when does it support re-auth', () => { + const connAuth = { myAuth: 'auth' } + const acquireAuth = connAuth + + it('should return connection when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + const auth = acquireAuth + + const delegatedConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + allowStickyConnection, + auth + }) + + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) + }) + + it('should return connection when try switch user on acquire [expired rt]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + pool, + { + dba: { + [server1.asHostPort()]: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + } + ) + connectionProvider._useSeedRouter = false + + const auth = acquireAuth + + const delegatedConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) + }) + + it('should return delegated connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + { + dba: { + [server0.asHostPort()]: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + }, + pool + ) + + const auth = acquireAuth + + const delegatedConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) + }) + + it('should delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [], + { + dba: { + [server0.asHostPort()]: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) + } + }, + pool + ) + + const auth = acquireAuth + + const delegatedConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + allowStickyConnection, + auth + }) + + expect(delegatedConnection).toBeInstanceOf(DelegateConnection) + expect(delegatedConnection._delegate).toBe(connection) + expect(connection._sticky).toEqual(false) + }) + }) + }) }) function newPool ({ create, config } = {}) { @@ -3834,7 +4242,7 @@ class FakeConnection extends Connection { class FakeRediscovery { constructor (routerToRoutingTable, error) { - this._routerToRoutingTable = routerToRoutingTable + this._routerToRoutingTable = routerToRoutingTable || {} this._error = error } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 92a8f4558..e469ef271 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -52,7 +52,12 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { const connection = await this._connectionPool.acquire({ auth }, this._address) if (auth) { - const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + const stickyConnection = await this._getStickyConnection({ + auth, + connection, + address: this._address, + allowStickyConnection + }) if (stickyConnection) { return stickyConnection } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index cf4d4f5e0..adba0daa6 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -126,19 +126,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { return true } - async _createStickyConnection ({ address, auth }) { - const connection = await this._createChannelConnection(address) - connection._release = () => this._destroyConnection(connection) - this._openConnections[connection.id] = connection - - try { - return await connection.connect(this._userAgent, auth) - } catch (error) { - await this._destroyConnection(connection) - throw error - } - } - /** * Dispose of a connection. * @return {Connection} the connection to dispose. @@ -168,7 +155,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { return serverInfo } - async _getStickyConnection ({ auth, connection, allowStickyConnection }) { + async _getStickyConnection ({ auth, connection, address, allowStickyConnection }) { const connectionWithSameCredentials = object.equals(auth, connection.authToken) const shouldCreateStickyConnection = !connectionWithSameCredentials connection._sticky = connectionWithSameCredentials && !connection.supportsReAuth @@ -178,7 +165,9 @@ export default class PooledConnectionProvider extends ConnectionProvider { throw newError('Driver is connected to a database that does not support user switch.') } else if (allowStickyConnection === true && shouldCreateStickyConnection) { await connection._release() - return await this._createStickyConnection({ address: this._address, auth }) + connection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) + connection._sticky = true + return connection } else if (connection._sticky) { return connection } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index dc2ed55c4..6c9c6086c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -194,7 +194,12 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const connection = await this._connectionPool.acquire({ auth }, address) if (auth) { - const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + const stickyConnection = await this._getStickyConnection({ + auth, + connection, + address, + allowStickyConnection + }) if (stickyConnection) { return stickyConnection } @@ -553,12 +558,17 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth, allowStickyConnection) { try { - const connection = await this._connectionPool.acquire({ auth }, routerAddress) + let connection = await this._connectionPool.acquire({ auth }, routerAddress) if (auth) { - const stickyConnection = await this._getStickyConnection({ auth, connection, allowStickyConnection }) + const stickyConnection = await this._getStickyConnection({ + auth, + connection, + address: routerAddress, + allowStickyConnection + }) if (stickyConnection) { - return stickyConnection + connection = stickyConnection } } @@ -567,8 +577,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn) }) - const connectionProvider = new SingleConnectionProvider( - new DelegateConnection(connection, databaseSpecificErrorHandler)) + const delegateConnection = !connection._sticky + ? new DelegateConnection(connection, databaseSpecificErrorHandler) + : new DelegateConnection(connection) + + const connectionProvider = new SingleConnectionProvider(delegateConnection) const protocolVersion = connection.protocol().version if (protocolVersion < 4.0) { From d7bdab4b55918fe1944904730674068bec781c11 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 6 Feb 2023 11:27:44 +0100 Subject: [PATCH 38/70] Add `supportsSessionAuth` --- .../connection-provider-direct.js | 13 ++++++++++++- .../connection-provider-routing.js | 9 ++++++++- packages/core/src/connection-provider.ts | 12 +++++++++++- packages/core/src/driver.ts | 13 +++++++++++++ packages/core/test/driver.test.ts | 17 +++++++++++++++++ .../connection-provider-direct.js | 13 ++++++++++++- .../connection-provider-routing.js | 9 ++++++++- .../lib/core/connection-provider.ts | 12 +++++++++++- packages/neo4j-driver-deno/lib/core/driver.ts | 13 +++++++++++++ 9 files changed, 105 insertions(+), 6 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 04a88076f..c03304266 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -26,7 +26,12 @@ import { import { internal, error } from 'neo4j-driver-core' const { - constants: { BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, BOLT_PROTOCOL_V4_4 } + constants: { + BOLT_PROTOCOL_V3, + BOLT_PROTOCOL_V4_0, + BOLT_PROTOCOL_V4_4, + BOLT_PROTOCOL_V5_1 + } } = internal const { SERVICE_UNAVAILABLE } = error @@ -128,6 +133,12 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } + async supportsSessionAuth () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V5_1 + ) + } + async verifyConnectivityAndGetServerInfo () { return await this._verifyConnectivityAndGetServerVersion({ address: this._address }) } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index c7c3b3ed1..1b0b71539 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -37,7 +37,8 @@ const { ACCESS_MODE_WRITE: WRITE, BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, - BOLT_PROTOCOL_V4_4 + BOLT_PROTOCOL_V4_4, + BOLT_PROTOCOL_V5_1 } } = internal @@ -268,6 +269,12 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } + async supportsSessionAuth () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V5_1 + ) + } + getNegotiatedProtocolVersion () { return new Promise((resolve, reject) => { this._hasProtocolVersion(resolve) diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index 9c7d14518..1466d1c8e 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -24,7 +24,7 @@ import { ServerInfo } from './result-summary' import { AuthToken } from './types' /** - * Inteface define a common way to acquire a connection + * Interface define a common way to acquire a connection * * @private */ @@ -88,6 +88,16 @@ class ConnectionProvider { throw Error('Not implemented') } + /** + * This method checks whether the driver session re-auth functionality + * by checking protocol handshake result + * + * @returns {Promise} + */ + supportsSessionAuth (): Promise { + throw Error('Not implemented') + } + /** * This method verifies the connectivity of the database by trying to acquire a connection * for each server available in the cluster. diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 8fdd2392c..97daa3740 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -627,6 +627,19 @@ class Driver { return connectionProvider.supportsUserImpersonation() } + /** + * Returns whether the driver session re-auth functionality capabilities based on the protocol + * version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} promise resolved with a boolean or rejected with error. + */ + supportsSessionAuth (): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.supportsSessionAuth() + } + /** * Returns the protocol version negotiated via handshake. * diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index 44de0c37c..519fea041 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -247,6 +247,23 @@ describe('Driver', () => { promise?.catch(_ => 'Do nothing').finally(() => {}) }) + it.each([ + ['Promise.resolve(true)', Promise.resolve(true)], + ['Promise.resolve(false)', Promise.resolve(false)], + [ + "Promise.reject(newError('something went wrong'))", + Promise.reject(newError('something went wrong')) + ] + ])('.supportsSessionAuth() => %s', (_, expectedPromise) => { + connectionProvider.supportsSessionAuth = jest.fn(() => expectedPromise) + + const promise: Promise | undefined = driver?.supportsSessionAuth() + + expect(promise).toBe(expectedPromise) + + promise?.catch(_ => 'Do nothing').finally(() => {}) + }) + it.each([ [{ encrypted: true }, true], [{ encrypted: false }, false], diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index e469ef271..6de4e9fd9 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -26,7 +26,12 @@ import { import { internal, error } from '../../core/index.ts' const { - constants: { BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, BOLT_PROTOCOL_V4_4 } + constants: { + BOLT_PROTOCOL_V3, + BOLT_PROTOCOL_V4_0, + BOLT_PROTOCOL_V4_4, + BOLT_PROTOCOL_V5_1 + } } = internal const { SERVICE_UNAVAILABLE } = error @@ -128,6 +133,12 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } + async supportsSessionAuth () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V5_1 + ) + } + async verifyConnectivityAndGetServerInfo () { return await this._verifyConnectivityAndGetServerVersion({ address: this._address }) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 6c9c6086c..12ec8fdd8 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -37,7 +37,8 @@ const { ACCESS_MODE_WRITE: WRITE, BOLT_PROTOCOL_V3, BOLT_PROTOCOL_V4_0, - BOLT_PROTOCOL_V4_4 + BOLT_PROTOCOL_V4_4, + BOLT_PROTOCOL_V5_1 } } = internal @@ -268,6 +269,12 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } + async supportsSessionAuth () { + return await this._hasProtocolVersion( + version => version >= BOLT_PROTOCOL_V5_1 + ) + } + getNegotiatedProtocolVersion () { return new Promise((resolve, reject) => { this._hasProtocolVersion(resolve) diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts index 7dfc7547b..393ab2883 100644 --- a/packages/neo4j-driver-deno/lib/core/connection-provider.ts +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -24,7 +24,7 @@ import { ServerInfo } from './result-summary.ts' import { AuthToken } from './types.ts' /** - * Inteface define a common way to acquire a connection + * Interface define a common way to acquire a connection * * @private */ @@ -88,6 +88,16 @@ class ConnectionProvider { throw Error('Not implemented') } + /** + * This method checks whether the driver session re-auth functionality + * by checking protocol handshake result + * + * @returns {Promise} + */ + supportsSessionAuth (): Promise { + throw Error('Not implemented') + } + /** * This method verifies the connectivity of the database by trying to acquire a connection * for each server available in the cluster. diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 0b6cf4318..4bee0924a 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -627,6 +627,19 @@ class Driver { return connectionProvider.supportsUserImpersonation() } + /** + * Returns whether the driver session re-auth functionality capabilities based on the protocol + * version negotiated via handshake. + * + * Note that this function call _always_ causes a round-trip to the server. + * + * @returns {Promise} promise resolved with a boolean or rejected with error. + */ + supportsSessionAuth (): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.supportsSessionAuth() + } + /** * Returns the protocol version negotiated via handshake. * From 2bbc14a4727672a3434f88d0e9486a58fb6d17a4 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 6 Feb 2023 12:09:56 +0100 Subject: [PATCH 39/70] Add CheckSessionAuthSupport to testkit --- packages/testkit-backend/src/request-handlers-rx.js | 1 + packages/testkit-backend/src/request-handlers.js | 10 ++++++++++ packages/testkit-backend/src/responses.js | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 5879f8b3e..1061a3ffb 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -11,6 +11,7 @@ export { VerifyConnectivity, GetServerInfo, CheckMultiDBSupport, + CheckSessionAuthSupport, ResolverResolutionCompleted, GetRoutingTable, ForcedRoutingTableUpdate, diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 2ea3c7cf3..02f2c4966 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -417,6 +417,16 @@ export function CheckMultiDBSupport (_, context, { driverId }, wire) { .catch(error => wire.writeError(error)) } +export function CheckSessionAuthSupport (_, context, { driverId }, wire) { + const driver = context.getDriver(driverId) + return driver + .supportsSessionAuth() + .then(available => + wire.writeResponse(responses.SessionAuthSupport({ id: driverId, available })) + ) + .catch(error => wire.writeError(error)) +} + export function ResolverResolutionCompleted ( _, context, diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index f6d6a6e33..18d823144 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -77,6 +77,10 @@ export function MultiDBSupport ({ id, available }) { return response('MultiDBSupport', { id, available }) } +export function SessionAuthSupport ({ id, available }) { + return response('SessionAuthSupport', { id, available }) +} + export function RoutingTable ({ routingTable }) { const serverAddressToString = serverAddress => serverAddress.asHostPort() return response('RoutingTable', { From 733e4e2d51ce3ac85c377107da6acfaebfb2923f Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 6 Feb 2023 13:23:59 +0100 Subject: [PATCH 40/70] Add tests for session configuration --- packages/core/src/types.ts | 2 +- packages/core/test/driver.test.ts | 20 +++++ packages/core/test/session.test.ts | 90 +++++++++++++++++++- packages/neo4j-driver-deno/lib/core/types.ts | 2 +- 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index ccd93ead2..2ad691d27 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -43,7 +43,7 @@ export type TrustStrategy = export interface Parameters { [key: string]: any } export interface AuthToken { scheme: string - principal: string + principal?: string credentials: string realm?: string parameters?: Parameters diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index 519fea041..e13a77142 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -82,6 +82,26 @@ describe('Driver', () => { expect(createSession).toHaveBeenCalledWith(expectedSessionParams()) }) + it('should create the session with auth', () => { + const auth = { + scheme: 'basic', + principal: 'the imposter', + credentials: 'super safe password' + } + + const session = driver?.session({ auth }) + + expect(session).not.toBeUndefined() + expect(createSession).toHaveBeenCalledWith(expectedSessionParams({ auth })) + }) + + it('should create the session without auth', () => { + const session = driver?.session() + + expect(session).not.toBeUndefined() + expect(createSession).toHaveBeenCalledWith(expectedSessionParams()) + }) + it.each([ [undefined, Bookmarks.empty()], [null, Bookmarks.empty()], diff --git a/packages/core/test/session.test.ts b/packages/core/test/session.test.ts index d554468ec..803d9c86d 100644 --- a/packages/core/test/session.test.ts +++ b/packages/core/test/session.test.ts @@ -20,6 +20,7 @@ import { ConnectionProvider, Session, Connection, TransactionPromise, Transactio import { bookmarks } from '../src/internal' import { ACCESS_MODE_READ, FETCH_ALL } from '../src/internal/constants' import ManagedTransaction from '../src/transaction-managed' +import { AuthToken } from '../src/types' import FakeConnection from './utils/connection.fake' import { validNotificationFilters } from './utils/notification-filters.fixtures' @@ -432,6 +433,47 @@ describe('session', () => { expect(updateBookmarksSpy).not.toBeCalled() }) + + it('should acquire connection with auth', async () => { + const auth = { + scheme: 'bearer', + credentials: 'bearer some-nice-token' + } + const connection = mockBeginWithSuccess(newFakeConnection()) + + const { session, connectionProvider } = setupSession({ + connection, + auth, + beginTx: false, + database: 'neo4j' + }) + + await session.beginTransaction() + + expect(connectionProvider.acquireConnection).toBeCalledWith( + expect.objectContaining({ auth }) + ) + }) + + it('should acquire connection without auth', async () => { + const auth = { + scheme: 'bearer', + credentials: 'bearer some-nice-token' + } + const connection = mockBeginWithSuccess(newFakeConnection()) + + const { session, connectionProvider } = setupSession({ + connection, + beginTx: false, + database: 'neo4j' + }) + + await session.beginTransaction() + + expect(connectionProvider.acquireConnection).not.toBeCalledWith( + expect.objectContaining({ auth }) + ) + }) }) describe('.commit()', () => { @@ -835,6 +877,47 @@ describe('session', () => { }) ) }) + + it('should acquire with auth', async () => { + const auth = { + scheme: 'bearer', + credentials: 'bearer some-nice-token' + } + const connection = newFakeConnection() + + const { session, connectionProvider } = setupSession({ + connection, + auth, + beginTx: false, + database: 'neo4j' + }) + + await session.run('query') + + expect(connectionProvider.acquireConnection).toBeCalledWith( + expect.objectContaining({ auth }) + ) + }) + + it('should acquire without auth', async () => { + const auth = { + scheme: 'bearer', + credentials: 'bearer some-nice-token' + } + const connection = newFakeConnection() + + const { session, connectionProvider } = setupSession({ + connection, + beginTx: false, + database: 'neo4j' + }) + + await session.run('query') + + expect(connectionProvider.acquireConnection).not.toBeCalledWith( + expect.objectContaining({ auth }) + ) + }) }) }) @@ -887,7 +970,8 @@ function setupSession ({ database = '', lastBookmarks = bookmarks.Bookmarks.empty(), bookmarkManager, - notificationFilter + notificationFilter, + auth }: { connection: Connection beginTx?: boolean @@ -896,6 +980,7 @@ function setupSession ({ database?: string bookmarkManager?: BookmarkManager notificationFilter?: NotificationFilter + auth?: AuthToken }): { session: Session, connectionProvider: ConnectionProvider } { const connectionProvider = new ConnectionProvider() connectionProvider.acquireConnection = jest.fn(async () => await Promise.resolve(connection)) @@ -910,7 +995,8 @@ function setupSession ({ reactive: false, bookmarks: lastBookmarks, bookmarkManager, - notificationFilter + notificationFilter, + auth }) if (beginTx) { diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index ccd93ead2..2ad691d27 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -43,7 +43,7 @@ export type TrustStrategy = export interface Parameters { [key: string]: any } export interface AuthToken { scheme: string - principal: string + principal?: string credentials: string realm?: string parameters?: Parameters From ce4806bf310ccab037ca90935a5223ff12df87d8 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 6 Feb 2023 13:43:40 +0100 Subject: [PATCH 41/70] Add user switching to RxSession --- packages/neo4j-driver/src/driver.js | 4 +++- packages/neo4j-driver/test/driver.test.js | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/neo4j-driver/src/driver.js b/packages/neo4j-driver/src/driver.js index f91638af4..c71621b9d 100644 --- a/packages/neo4j-driver/src/driver.js +++ b/packages/neo4j-driver/src/driver.js @@ -59,7 +59,8 @@ class Driver extends CoreDriver { fetchSize, impersonatedUser, bookmarkManager, - notificationFilter + notificationFilter, + auth } = {}) { return new RxSession({ session: this._newSession({ @@ -67,6 +68,7 @@ class Driver extends CoreDriver { bookmarkOrBookmarks: bookmarks, database, impersonatedUser, + auth, reactive: false, fetchSize: validateFetchSizeValue(fetchSize, this._config.fetchSize), bookmarkManager, diff --git a/packages/neo4j-driver/test/driver.test.js b/packages/neo4j-driver/test/driver.test.js index 36b16c712..fe19b49d8 100644 --- a/packages/neo4j-driver/test/driver.test.js +++ b/packages/neo4j-driver/test/driver.test.js @@ -145,6 +145,20 @@ describe('#unit driver', () => { }) }) + it('should create session using auth', () => { + driver = neo4j.driver( + `neo4j+ssc://${sharedNeo4j.hostname}`, + sharedNeo4j.authToken + ) + + const auth = { scheme: 'none' } + + const session = driver.rxSession({ auth }) + + expect(session._session._readConnectionHolder._auth).toEqual(auth) + expect(session._session._writeConnectionHolder._auth).toEqual(auth) + }) + ;[ [manager, manager], [undefined, undefined] From f9933f8d8da165154bd038f651bf4ef990a167fd Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 6 Feb 2023 18:28:14 +0100 Subject: [PATCH 42/70] VerifyAuthorization --- .../connection-provider-pooled.js | 20 +++++++++++++++++++ packages/core/src/connection-provider.ts | 16 +++++++++++++++ packages/core/src/driver.ts | 16 +++++++++++++++ .../connection-provider-pooled.js | 20 +++++++++++++++++++ .../lib/core/connection-provider.ts | 16 +++++++++++++++ packages/neo4j-driver-deno/lib/core/driver.ts | 16 +++++++++++++++ .../src/request-handlers-rx.js | 8 +++++++- .../testkit-backend/src/request-handlers.js | 12 +++++++++++ packages/testkit-backend/src/responses.js | 4 ++++ 9 files changed, 127 insertions(+), 1 deletion(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 974330c7c..fc3662741 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -24,6 +24,13 @@ import AuthenticationProvider from './authentication-provider' import { object } from '../lang' const { SERVICE_UNAVAILABLE } = error +const AUTHENTICATION_ERRORS = [ + 'Neo.ClientError.Security.CredentialsExpired', + 'Neo.ClientError.Security.Forbidden', + 'Neo.ClientError.Security.TokenExpired', + 'Neo.ClientError.Security.Unauthorized' +] + export default class PooledConnectionProvider extends ConnectionProvider { constructor ( { id, config, log, userAgent, authTokenProvider, newPool = (...args) => new Pool(...args) }, @@ -62,6 +69,19 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._openConnections = {} } + async verifyAuthentication ({ auth, database, accessMode } = {}) { + try { + const connection = await this.acquireConnection({ accessMode, database, auth }) + await connection._release() + return true + } catch (error) { + if (AUTHENTICATION_ERRORS.includes(error.code)) { + return false + } + throw error + } + } + _createConnectionErrorHandler () { return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) } diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index 1466d1c8e..d145f58ea 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -112,6 +112,22 @@ class ConnectionProvider { throw Error('Not implemented') } + /** + * This method verifies the authorization credentials work by trying to acquire a connection + * to one of the servers with the given credentials. + * + * @param {object} param - object parameter + * @property {AuthToken} param.auth - the target auth for the to-be-acquired connection + * @property {string} param.database - the target database for the to-be-acquired connection + * @property {string} param.accessMode - the access mode for the to-be-acquired connection + * + * @returns {Promise} promise resolved with true if succeed, false if failed with + * authentication issue and rejected with error if non-authentication error happens. + */ + verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string }): Promise { + throw Error('Not implemented') + } + /** * Returns the protocol version negotiated via handshake. * diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 97daa3740..33bf28a19 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -576,6 +576,22 @@ class Driver { return connectionProvider.verifyConnectivityAndGetServerInfo({ database, accessMode: READ }) } + /** + * This method verifies the authorization credentials work by trying to acquire a connection + * to one of the servers with the given credentials. + * + * @param {object} param - object parameter + * @property {AuthToken} param.auth - the target auth for the to-be-acquired connection + * @property {string} param.database - the target database for the to-be-acquired connection + * + * @returns {Promise} promise resolved with true if succeed, false if failed with + * authentication issue and rejected with error if non-authentication error happens. + */ + verifyAuthentication ({ database, auth }: { auth?: AuthToken, database?: string } = {}): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.verifyAuthentication({ database: database ?? '', auth, accessMode: READ }) + } + /** * Get ServerInfo for the giver database. * diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index adba0daa6..a418c9da0 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -24,6 +24,13 @@ import AuthenticationProvider from './authentication-provider.js' import { object } from '../lang/index.js' const { SERVICE_UNAVAILABLE } = error +const AUTHENTICATION_ERRORS = [ + 'Neo.ClientError.Security.CredentialsExpired', + 'Neo.ClientError.Security.Forbidden', + 'Neo.ClientError.Security.TokenExpired', + 'Neo.ClientError.Security.Unauthorized' +] + export default class PooledConnectionProvider extends ConnectionProvider { constructor ( { id, config, log, userAgent, authTokenProvider, newPool = (...args) => new Pool(...args) }, @@ -62,6 +69,19 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._openConnections = {} } + async verifyAuthentication ({ auth, database, accessMode } = {}) { + try { + const connection = await this.acquireConnection({ accessMode, database, auth }) + await connection._release() + return true + } catch (error) { + if (AUTHENTICATION_ERRORS.includes(error.code)) { + return false + } + throw error + } + } + _createConnectionErrorHandler () { return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) } diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts index 393ab2883..69fceb8be 100644 --- a/packages/neo4j-driver-deno/lib/core/connection-provider.ts +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -112,6 +112,22 @@ class ConnectionProvider { throw Error('Not implemented') } + /** + * This method verifies the authorization credentials work by trying to acquire a connection + * to one of the servers with the given credentials. + * + * @param {object} param - object parameter + * @property {AuthToken} param.auth - the target auth for the to-be-acquired connection + * @property {string} param.database - the target database for the to-be-acquired connection + * @property {string} param.accessMode - the access mode for the to-be-acquired connection + * + * @returns {Promise} promise resolved with true if succeed, false if failed with + * authentication issue and rejected with error if non-authentication error happens. + */ + verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string }): Promise { + throw Error('Not implemented') + } + /** * Returns the protocol version negotiated via handshake. * diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 4bee0924a..7f433e766 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -576,6 +576,22 @@ class Driver { return connectionProvider.verifyConnectivityAndGetServerInfo({ database, accessMode: READ }) } + /** + * This method verifies the authorization credentials work by trying to acquire a connection + * to one of the servers with the given credentials. + * + * @param {object} param - object parameter + * @property {AuthToken} param.auth - the target auth for the to-be-acquired connection + * @property {string} param.database - the target database for the to-be-acquired connection + * + * @returns {Promise} promise resolved with true if succeed, false if failed with + * authentication issue and rejected with error if non-authentication error happens. + */ + verifyAuthentication ({ database, auth }: { auth?: AuthToken, database?: string } = {}): Promise { + const connectionProvider = this._getOrCreateConnectionProvider() + return connectionProvider.verifyAuthentication({ database: database ?? '', auth, accessMode: READ }) + } + /** * Get ServerInfo for the giver database. * diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index 1061a3ffb..df64974a2 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -9,6 +9,7 @@ export { StartTest, GetFeatures, VerifyConnectivity, + VerifyAuthentication, GetServerInfo, CheckMultiDBSupport, CheckSessionAuthSupport, @@ -60,6 +61,10 @@ export function NewSession ({ neo4j }, context, data, wire) { disabledCategories: data.notificationsDisabledCategories } } + const auth = data.authorizationToken != null + ? context.binder.parseAuthToken(data.authorizationToken.data) + : undefined + const driver = context.getDriver(driverId) const session = driver.rxSession({ defaultAccessMode: accessMode, @@ -68,7 +73,8 @@ export function NewSession ({ neo4j }, context, data, wire) { fetchSize, impersonatedUser, bookmarkManager, - notificationFilter + notificationFilter, + auth }) const id = context.addSession(session) wire.writeResponse(responses.Session({ id })) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 02f2c4966..3a0903c04 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -399,6 +399,18 @@ export function VerifyConnectivity (_, context, { driverId }, wire) { .catch(error => wire.writeError(error)) } +export function VerifyAuthentication (_, context, { driverId, auth_token: authToken }, wire) { + const auth = authToken != null + ? context.binder.parseAuthToken(authToken.data) + : undefined + + const driver = context.getDriver(driverId) + return driver + .verifyAuthentication({ auth }) + .then(authenticated => responses.DriverIsAuthenticated({ id: driverId, authenticated })) + .catch(error => wire.writeError(error)) +} + export function GetServerInfo (_, context, { driverId }, wire) { const driver = context.getDriver(driverId) return driver diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index 18d823144..5c21fe2cc 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -111,6 +111,10 @@ export function AuthTokenProviderRequest ({ id, authTokenProviderId }) { return response('AuthTokenProviderRequest', { id, authTokenProviderId }) } +export function DriverIsAuthenticated ({ id, authenticated }) { + return response('DriverIsAuthenticated', { id, authenticated }) +} + // Testkit controller messages export function RunTest () { return response('RunTest', null) From 46f99edf581d101cc17e6774853c38bb57dc1f8c Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 8 Feb 2023 11:46:33 +0100 Subject: [PATCH 43/70] Add backwardsCompatibleAuth flag to the driver configuration --- .../authentication-provider.js | 1 + .../connection-provider-pooled.js | 4 +-- .../connection-provider-routing.js | 4 ++- packages/core/src/connection-provider.ts | 3 ++- packages/core/src/driver.ts | 26 +++++++++++++++++-- .../core/src/internal/connection-holder.ts | 11 ++++++-- packages/core/test/driver.test.ts | 4 ++- .../bolt-connection/bolt/bolt-protocol-v1.js | 2 +- .../authentication-provider.js | 1 + .../connection-provider-pooled.js | 4 +-- .../connection-provider-routing.js | 4 ++- .../lib/core/connection-provider.ts | 3 ++- packages/neo4j-driver-deno/lib/core/driver.ts | 26 +++++++++++++++++-- .../lib/core/internal/connection-holder.ts | 11 ++++++-- .../neo4j-driver-lite/test/unit/index.test.ts | 4 ++- .../neo4j-driver/test/types/driver.test.ts | 2 +- .../testkit-backend/src/controller/local.js | 1 + .../testkit-backend/src/feature/common.js | 1 + .../testkit-backend/src/request-handlers.js | 8 +++--- 19 files changed, 97 insertions(+), 23 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index 12904665c..3ad938daa 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -51,6 +51,7 @@ export default class AuthenticationProvider { handleError ({ connection, code }) { if ( + connection && object.equals(connection.authToken, this._authToken) && [ 'Neo.ClientError.Security.Unauthorized', diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index fc3662741..be31e44ee 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -69,9 +69,9 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._openConnections = {} } - async verifyAuthentication ({ auth, database, accessMode } = {}) { + async verifyAuthentication ({ auth, database, accessMode, allowStickyConnection } = {}) { try { - const connection = await this.acquireConnection({ accessMode, database, auth }) + const connection = await this.acquireConnection({ accessMode, database, auth, allowStickyConnection }) await connection._release() return true } catch (error) { diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 1b0b71539..1578e99b0 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -123,7 +123,9 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._connectionPool.apply(address, (conn) => { conn.authToken = null }) } - connection.close().catch(() => undefined) + if (connection) { + connection.close().catch(() => undefined) + } return error } diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index d145f58ea..c5f2b9df2 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -120,11 +120,12 @@ class ConnectionProvider { * @property {AuthToken} param.auth - the target auth for the to-be-acquired connection * @property {string} param.database - the target database for the to-be-acquired connection * @property {string} param.accessMode - the access mode for the to-be-acquired connection + * @property {boolean} param.allowStickyConnection - enables the usage of sticky connection for backwards compatibility * * @returns {Promise} promise resolved with true if succeed, false if failed with * authentication issue and rejected with error if non-authentication error happens. */ - verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string }): Promise { + verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string, allowStickyConnection?: boolean }): Promise { throw Error('Not implemented') } diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 33bf28a19..3e1b19740 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -195,6 +195,24 @@ class SessionConfig { */ this.impersonatedUser = undefined + /** + * The {@link AuthToken} which will be used for the duration of the session. + * + * By default, the session will use connections authenticated with {@link AuthToken} configured in the + * driver creation. This configuration allows switch user and/or authorization information for the + * session lifetime. + * + * **Warning**: This option is only enable by default when the driver is connected with Neo4j Database servers + * which supports Bolt 5.1 and onwards. For enabling backwards compatibility mode, please configure the + * driver with `backwardsCompatibleAuth` enable. Beware, the backwards compatible mode comes with + * a huge performance penalty since it uses a new connection for each unit of work run in a session + * configured with an auth. + * + * @type {AuthToken|undefined} + * @see {@link driver} + */ + this.auth = undefined + /** * The record fetch size of each batch of this session. * @@ -587,9 +605,13 @@ class Driver { * @returns {Promise} promise resolved with true if succeed, false if failed with * authentication issue and rejected with error if non-authentication error happens. */ - verifyAuthentication ({ database, auth }: { auth?: AuthToken, database?: string } = {}): Promise { + async verifyAuthentication ({ database, auth }: { auth?: AuthToken, database?: string } = {}): Promise { const connectionProvider = this._getOrCreateConnectionProvider() - return connectionProvider.verifyAuthentication({ database: database ?? '', auth, accessMode: READ }) + return await connectionProvider.verifyAuthentication({ + database: database ?? 'system', + auth, + accessMode: READ + }) } /** diff --git a/packages/core/src/internal/connection-holder.ts b/packages/core/src/internal/connection-holder.ts index 787a61f86..652fade05 100644 --- a/packages/core/src/internal/connection-holder.ts +++ b/packages/core/src/internal/connection-holder.ts @@ -87,6 +87,7 @@ class ConnectionHolder implements ConnectionHolderInterface { private readonly _getConnectionAcquistionBookmarks: () => Promise private readonly _onDatabaseNameResolved?: (databaseName?: string) => void private readonly _auth?: AuthToken + private readonly _backwardsCompatibleAuth?: boolean private _closed: boolean /** @@ -99,6 +100,8 @@ class ConnectionHolder implements ConnectionHolderInterface { * @property {string?} params.impersonatedUser - the user which will be impersonated * @property {function(databaseName:string)} params.onDatabaseNameResolved - callback called when the database name is resolved * @property {function():Promise} params.getConnectionAcquistionBookmarks - called for getting Bookmarks for acquiring connections + * @property {AuthToken} params.auth - the target auth for the to-be-acquired connection + * @property {boolean} params.backwardsCompatibleAuth - Enables backwards compatible re-auth */ constructor ({ mode = ACCESS_MODE_WRITE, @@ -108,7 +111,8 @@ class ConnectionHolder implements ConnectionHolderInterface { impersonatedUser, onDatabaseNameResolved, getConnectionAcquistionBookmarks, - auth + auth, + backwardsCompatibleAuth }: { mode?: string database?: string @@ -118,6 +122,7 @@ class ConnectionHolder implements ConnectionHolderInterface { onDatabaseNameResolved?: (databaseName?: string) => void getConnectionAcquistionBookmarks?: () => Promise auth?: AuthToken + backwardsCompatibleAuth?: boolean } = {}) { this._mode = mode this._closed = false @@ -129,6 +134,7 @@ class ConnectionHolder implements ConnectionHolderInterface { this._connectionPromise = Promise.resolve(null) this._onDatabaseNameResolved = onDatabaseNameResolved this._auth = auth + this._backwardsCompatibleAuth = backwardsCompatibleAuth this._getConnectionAcquistionBookmarks = getConnectionAcquistionBookmarks ?? (() => Promise.resolve(Bookmarks.empty())) } @@ -174,7 +180,8 @@ class ConnectionHolder implements ConnectionHolderInterface { bookmarks: await this._getBookmarks(), impersonatedUser: this._impersonatedUser, onDatabaseNameResolved: this._onDatabaseNameResolved, - auth: this._auth + auth: this._auth, + allowStickyConnection: this._backwardsCompatibleAuth }) } diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index e13a77142..062aabab5 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -617,7 +617,8 @@ describe('Driver', () => { fetchSize: 1000, maxConnectionLifetime: 3600000, maxConnectionPoolSize: 100, - connectionTimeout: 30000 + connectionTimeout: 30000, + backwardsCompatibleAuth: false }, connectionProvider, database: '', @@ -625,6 +626,7 @@ describe('Driver', () => { mode: 'WRITE', reactive: false, impersonatedUser: undefined, + backwardsCompatibleAuth: false, ...extra } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js index 1212bb019..4f7884652 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -236,7 +236,7 @@ export default class BoltProtocol { // TODO: Verify the Neo4j version in the message const error = newError( - 'Driver is connected to a database that does not support logon. ' + + 'Driver is connected to a database that does not support login. ' + 'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.' ) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index fc380ebc6..5ee5ada4b 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -51,6 +51,7 @@ export default class AuthenticationProvider { handleError ({ connection, code }) { if ( + connection && object.equals(connection.authToken, this._authToken) && [ 'Neo.ClientError.Security.Unauthorized', diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index a418c9da0..abed26010 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -69,9 +69,9 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._openConnections = {} } - async verifyAuthentication ({ auth, database, accessMode } = {}) { + async verifyAuthentication ({ auth, database, accessMode, allowStickyConnection } = {}) { try { - const connection = await this.acquireConnection({ accessMode, database, auth }) + const connection = await this.acquireConnection({ accessMode, database, auth, allowStickyConnection }) await connection._release() return true } catch (error) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 12ec8fdd8..55f481b1c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -123,7 +123,9 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._connectionPool.apply(address, (conn) => { conn.authToken = null }) } - connection.close().catch(() => undefined) + if (connection) { + connection.close().catch(() => undefined) + } return error } diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts index 69fceb8be..a03a86fb4 100644 --- a/packages/neo4j-driver-deno/lib/core/connection-provider.ts +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -120,11 +120,12 @@ class ConnectionProvider { * @property {AuthToken} param.auth - the target auth for the to-be-acquired connection * @property {string} param.database - the target database for the to-be-acquired connection * @property {string} param.accessMode - the access mode for the to-be-acquired connection + * @property {boolean} param.allowStickyConnection - enables the usage of sticky connection for backwards compatibility * * @returns {Promise} promise resolved with true if succeed, false if failed with * authentication issue and rejected with error if non-authentication error happens. */ - verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string }): Promise { + verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string, allowStickyConnection?: boolean }): Promise { throw Error('Not implemented') } diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 7f433e766..2f854c007 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -195,6 +195,24 @@ class SessionConfig { */ this.impersonatedUser = undefined + /** + * The {@link AuthToken} which will be used for the duration of the session. + * + * By default, the session will use connections authenticated with {@link AuthToken} configured in the + * driver creation. This configuration allows switch user and/or authorization information for the + * session lifetime. + * + * **Warning**: This option is only enable by default when the driver is connected with Neo4j Database servers + * which supports Bolt 5.1 and onwards. For enabling backwards compatibility mode, please configure the + * driver with `backwardsCompatibleAuth` enable. Beware, the backwards compatible mode comes with + * a huge performance penalty since it uses a new connection for each unit of work run in a session + * configured with an auth. + * + * @type {AuthToken|undefined} + * @see {@link driver} + */ + this.auth = undefined + /** * The record fetch size of each batch of this session. * @@ -587,9 +605,13 @@ class Driver { * @returns {Promise} promise resolved with true if succeed, false if failed with * authentication issue and rejected with error if non-authentication error happens. */ - verifyAuthentication ({ database, auth }: { auth?: AuthToken, database?: string } = {}): Promise { + async verifyAuthentication ({ database, auth }: { auth?: AuthToken, database?: string } = {}): Promise { const connectionProvider = this._getOrCreateConnectionProvider() - return connectionProvider.verifyAuthentication({ database: database ?? '', auth, accessMode: READ }) + return await connectionProvider.verifyAuthentication({ + database: database ?? 'system', + auth, + accessMode: READ + }) } /** diff --git a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts index 6cc6c7e5b..bb285163b 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts @@ -87,6 +87,7 @@ class ConnectionHolder implements ConnectionHolderInterface { private readonly _getConnectionAcquistionBookmarks: () => Promise private readonly _onDatabaseNameResolved?: (databaseName?: string) => void private readonly _auth?: AuthToken + private readonly _backwardsCompatibleAuth?: boolean private _closed: boolean /** @@ -99,6 +100,8 @@ class ConnectionHolder implements ConnectionHolderInterface { * @property {string?} params.impersonatedUser - the user which will be impersonated * @property {function(databaseName:string)} params.onDatabaseNameResolved - callback called when the database name is resolved * @property {function():Promise} params.getConnectionAcquistionBookmarks - called for getting Bookmarks for acquiring connections + * @property {AuthToken} params.auth - the target auth for the to-be-acquired connection + * @property {boolean} params.backwardsCompatibleAuth - Enables backwards compatible re-auth */ constructor ({ mode = ACCESS_MODE_WRITE, @@ -108,7 +111,8 @@ class ConnectionHolder implements ConnectionHolderInterface { impersonatedUser, onDatabaseNameResolved, getConnectionAcquistionBookmarks, - auth + auth, + backwardsCompatibleAuth }: { mode?: string database?: string @@ -118,6 +122,7 @@ class ConnectionHolder implements ConnectionHolderInterface { onDatabaseNameResolved?: (databaseName?: string) => void getConnectionAcquistionBookmarks?: () => Promise auth?: AuthToken + backwardsCompatibleAuth?: boolean } = {}) { this._mode = mode this._closed = false @@ -129,6 +134,7 @@ class ConnectionHolder implements ConnectionHolderInterface { this._connectionPromise = Promise.resolve(null) this._onDatabaseNameResolved = onDatabaseNameResolved this._auth = auth + this._backwardsCompatibleAuth = backwardsCompatibleAuth this._getConnectionAcquistionBookmarks = getConnectionAcquistionBookmarks ?? (() => Promise.resolve(Bookmarks.empty())) } @@ -174,7 +180,8 @@ class ConnectionHolder implements ConnectionHolderInterface { bookmarks: await this._getBookmarks(), impersonatedUser: this._impersonatedUser, onDatabaseNameResolved: this._onDatabaseNameResolved, - auth: this._auth + auth: this._auth, + allowStickyConnection: this._backwardsCompatibleAuth }) } diff --git a/packages/neo4j-driver-lite/test/unit/index.test.ts b/packages/neo4j-driver-lite/test/unit/index.test.ts index a5ebbc4aa..b9b6fd4a1 100644 --- a/packages/neo4j-driver-lite/test/unit/index.test.ts +++ b/packages/neo4j-driver-lite/test/unit/index.test.ts @@ -255,7 +255,9 @@ describe('index', () => { supportsTransactionConfig: async () => true, supportsUserImpersonation: async () => true, verifyConnectivityAndGetServerInfo: async () => new ServerInfo({}), - getNegotiatedProtocolVersion: async () => 5.0 + getNegotiatedProtocolVersion: async () => 5.0, + verifyAuthentication: async () => true, + supportsSessionAuth: async () => true } }) expect(session).toBeDefined() diff --git a/packages/neo4j-driver/test/types/driver.test.ts b/packages/neo4j-driver/test/types/driver.test.ts index 577d13465..a39445e5d 100644 --- a/packages/neo4j-driver/test/types/driver.test.ts +++ b/packages/neo4j-driver/test/types/driver.test.ts @@ -38,7 +38,7 @@ const dummy: any = null const authToken: AuthToken = dummy const scheme: string = authToken.scheme -const principal: string = authToken.principal +const principal: string | undefined = authToken.principal const credentials: string = authToken.credentials const realm1: undefined = authToken.realm as undefined const realm2: string = authToken.realm as string diff --git a/packages/testkit-backend/src/controller/local.js b/packages/testkit-backend/src/controller/local.js index 0d8f190ad..9513eab95 100644 --- a/packages/testkit-backend/src/controller/local.js +++ b/packages/testkit-backend/src/controller/local.js @@ -59,6 +59,7 @@ export default class LocalController extends Controller { } _writeError (contextId, e) { + console.trace(e) if (e.name) { if (isFrontendError(e)) { this._writeResponse(contextId, newResponse('FrontendError', { diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index a3bacadbd..2170ed3ce 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -25,6 +25,7 @@ const features = [ 'Feature:API:Driver.ExecuteQuery', 'Feature:API:Driver:NotificationsConfig', 'Feature:API:Driver:GetServerInfo', + 'Feature:API:Driver.VerifyAuthentication', 'Feature:API:Driver.VerifyConnectivity', 'Feature:API:Session:NotificationsConfig', 'Optimization:AuthPipelining', diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 3a0903c04..ba1f7fd5f 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -14,7 +14,8 @@ export function NewDriver ({ neo4j }, context, data, wire) { authorizationToken, authTokenProviderId, userAgent, - resolverRegistered + resolverRegistered, + backwardsCompatibleAuth } = data let parsedAuthToken = null @@ -40,6 +41,7 @@ export function NewDriver ({ neo4j }, context, data, wire) { userAgent, resolver, useBigInt: true, + backwardsCompatibleAuth, logging: neo4j.logging.console(context.logLevel || context.environmentLogLevel) } if ('encrypted' in data) { @@ -400,14 +402,14 @@ export function VerifyConnectivity (_, context, { driverId }, wire) { } export function VerifyAuthentication (_, context, { driverId, auth_token: authToken }, wire) { - const auth = authToken != null + const auth = authToken != null && authToken.data != null ? context.binder.parseAuthToken(authToken.data) : undefined const driver = context.getDriver(driverId) return driver .verifyAuthentication({ auth }) - .then(authenticated => responses.DriverIsAuthenticated({ id: driverId, authenticated })) + .then(authenticated => wire.writeResponse(responses.DriverIsAuthenticated({ id: driverId, authenticated }))) .catch(error => wire.writeError(error)) } From 368f8411299694dac7b20a41328cf018a96fb947 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 8 Feb 2023 17:59:34 +0100 Subject: [PATCH 44/70] VerifyAuth: refresh auth if needed --- .../connection-provider-pooled.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index be31e44ee..18cef41c5 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -66,13 +66,27 @@ export default class PooledConnectionProvider extends ConnectionProvider { config: PoolConfig.fromDriverConfig(config), log: this._log }) + this._userAgent = userAgent this._openConnections = {} } async verifyAuthentication ({ auth, database, accessMode, allowStickyConnection } = {}) { try { const connection = await this.acquireConnection({ accessMode, database, auth, allowStickyConnection }) - await connection._release() + const address = connection.address + const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogin() + try { + if (lastMessageIsNotLogin && connection.supportsReAuth) { + await connection.connect(this._userAgent, auth) + } + } finally { + await connection._release() + } + if (lastMessageIsNotLogin && !connection.supportsReAuth) { + const stickyConnection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) + stickyConnection._sticky = true + await stickyConnection._release() + } return true } catch (error) { if (AUTHENTICATION_ERRORS.includes(error.code)) { From 0e4e7c08947ac0471f90f504fc576940251cb9f1 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 21 Feb 2023 13:53:52 +0100 Subject: [PATCH 45/70] Add AuthTokenManager --- .../authentication-provider.js | 77 +------- .../connection-provider-direct.js | 4 +- .../connection-provider-pooled.js | 4 +- .../connection-provider-routing.js | 4 +- .../authorization-provider.test.js | 29 ++- packages/core/src/auth-token-manager.ts | 171 ++++++++++++++++++ packages/core/src/auth.ts | 5 + packages/core/src/index.ts | 5 + packages/core/src/internal/util.ts | 39 ++++ packages/core/src/types.ts | 8 - .../authentication-provider.js | 77 +------- .../connection-provider-direct.js | 4 +- .../connection-provider-pooled.js | 20 +- .../connection-provider-routing.js | 4 +- .../lib/core/auth-token-manager.ts | 171 ++++++++++++++++++ packages/neo4j-driver-deno/lib/core/auth.ts | 5 + packages/neo4j-driver-deno/lib/core/index.ts | 5 + .../lib/core/internal/util.ts | 39 ++++ packages/neo4j-driver-deno/lib/core/types.ts | 8 - packages/neo4j-driver-deno/lib/mod.ts | 59 ++++-- packages/neo4j-driver-lite/src/index.ts | 59 ++++-- packages/neo4j-driver/src/index.js | 46 +++-- packages/neo4j-driver/types/index.d.ts | 13 +- .../testkit-backend/src/request-handlers.js | 4 +- 24 files changed, 620 insertions(+), 240 deletions(-) create mode 100644 packages/core/src/auth-token-manager.ts create mode 100644 packages/neo4j-driver-deno/lib/core/auth-token-manager.ts diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index 3ad938daa..38adf36c3 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -17,17 +17,18 @@ * limitations under the License. */ +import { temporalAuthDataManager } from 'neo4j-driver-core' import { object } from '../lang' /** * Class which provides Authorization for {@link Connection} */ export default class AuthenticationProvider { - constructor ({ authTokenProvider, userAgent }) { - this._getAuthToken = authTokenProvider || (() => ({})) - this._renewableAuthToken = undefined + constructor ({ authTokenManager, userAgent }) { + this._authTokenManager = authTokenManager || temporalAuthDataManager({ + getAuthData: () => {} + }) this._userAgent = userAgent - this._refreshObserver = undefined } async authenticate ({ connection, auth }) { @@ -38,12 +39,10 @@ export default class AuthenticationProvider { return connection } - if (!this._authToken || this._isTokenExpired) { - await this._getFreshAuthToken() - } + const authToken = await this._authTokenManager.getToken() - if (!object.equals(this._renewableAuthToken.authToken, connection.authToken)) { - return await connection.connect(this._userAgent, this._authToken) + if (!object.equals(authToken, connection.authToken)) { + return await connection.connect(this._userAgent, authToken) } return connection @@ -52,70 +51,12 @@ export default class AuthenticationProvider { handleError ({ connection, code }) { if ( connection && - object.equals(connection.authToken, this._authToken) && [ 'Neo.ClientError.Security.Unauthorized', 'Neo.ClientError.Security.TokenExpired' ].includes(code) ) { - this._scheduleRefresh() - } - } - - get _authToken () { - if (this._renewableAuthToken) { - return this._renewableAuthToken.authToken - } - return undefined - } - - get _isTokenExpired () { - return !this._renewableAuthToken || - (this._renewableAuthToken.expectedExpirationTime && - this._renewableAuthToken.expectedExpirationTime < new Date()) - } - - async _getFreshAuthToken () { - if (this._isTokenExpired) { - const promise = new Promise((resolve, reject) => { - this._scheduleRefresh({ - onSuccess: resolve, - onError: reject - }) - }) - await promise - } - - return this._authToken - } - - _scheduleRefresh (observer) { - // there is no refresh schedule - if (!this._refreshObserver) { - const subscribers = [] - - this._refreshObserver = { - subscribe: (sub) => subscribers.push(sub), - notify: () => subscribers.forEach(sub => sub.onSuccess()), - notifyError: (e) => subscribers.forEach(sub => sub.onError(e)) - } - - Promise.resolve(this._getAuthToken()) - .then(token => { - this._renewableAuthToken = token - this._refreshObserver.notify() - return token - }) - .catch(e => { - this._refreshObserver.notifyError(e) - }) - .finally(() => { - this._refreshObserver = undefined - }) - } - - if (observer) { - this._refreshObserver.subscribe(observer) + this._authTokenManager.onTokenExpired(connection.authToken) } } } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index c03304266..752fa144e 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -37,8 +37,8 @@ const { const { SERVICE_UNAVAILABLE } = error export default class DirectConnectionProvider extends PooledConnectionProvider { - constructor ({ id, config, log, address, userAgent, authTokenProvider, newPool }) { - super({ id, config, log, userAgent, authTokenProvider, newPool }) + constructor ({ id, config, log, address, userAgent, authTokenManager, newPool }) { + super({ id, config, log, userAgent, authTokenManager, newPool }) this._address = address } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 18cef41c5..ec38c4634 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -33,7 +33,7 @@ const AUTHENTICATION_ERRORS = [ export default class PooledConnectionProvider extends ConnectionProvider { constructor ( - { id, config, log, userAgent, authTokenProvider, newPool = (...args) => new Pool(...args) }, + { id, config, log, userAgent, authTokenManager, newPool = (...args) => new Pool(...args) }, createChannelConnectionHook = null ) { super() @@ -41,7 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log - this._authenticationProvider = new AuthenticationProvider({ authTokenProvider, userAgent }) + this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent }) this._createChannelConnection = createChannelConnectionHook || (address => { diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 1578e99b0..33786c31b 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -67,11 +67,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider config, log, userAgent, - authTokenProvider, + authTokenManager, routingTablePurgeDelay, newPool }) { - super({ id, config, log, userAgent, authTokenProvider, newPool }, address => { + super({ id, config, log, userAgent, authTokenManager, newPool }, address => { return createChannelConnection( address, this._config, diff --git a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js index 2a2e03eb5..c73d96816 100644 --- a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js @@ -16,6 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { temporalAuthDataManager } from 'neo4j-driver-core' import AuthenticationProvider from '../../src/connection-provider/authentication-provider' describe('AuthenticationProvider', () => { @@ -592,14 +593,14 @@ describe('AuthenticationProvider', () => { expect(authTokenProvider).toHaveBeenCalled() // Test implementation details - expect(authenticationProvider._renewableAuthToken).toEqual(renewableAuthToken) + expect(authenticationProvider._authTokenManager._currentAuthData).toEqual(undefined) const newRenewableToken = toRenewableToken({ scheme: 'bearer', credentials: 'token2' }) newTokenPromiseState.resolve(newRenewableToken) await newTokenPromise - expect(authenticationProvider._renewableAuthToken).toBe(newRenewableToken) + expect(authenticationProvider._authTokenManager._currentAuthData).toBe(newRenewableToken) }) function shouldNotScheduleRefreshScenarios () { @@ -710,14 +711,15 @@ describe('AuthenticationProvider', () => { }) function createAuthenticationProvider (authTokenProvider, mocks) { + const authTokenManager = temporalAuthDataManager({ getAuthData: authTokenProvider }) const provider = new AuthenticationProvider({ - authTokenProvider, + authTokenManager, userAgent: USER_AGENT }) if (mocks) { - provider._renewableAuthToken = mocks.renewableAuthToken - provider._refreshObserver = mocks.refreshObserver + authTokenManager._currentAuthData = mocks.renewableAuthToken + authTokenManager._refreshObservable = mocks.refreshObserver } return provider @@ -732,18 +734,15 @@ describe('AuthenticationProvider', () => { return connection } - function toRenewableToken (authToken, expectedExpirationTime) { + function toRenewableToken (token, expiry) { return { - authToken, - expectedExpirationTime + token, + expiry } } - function toExpiredRenewableToken (authToken) { - return { - authToken, - expectedExpirationTime: new Date(new Date().getTime() - 1) - } + function toExpiredRenewableToken (token) { + return toRenewableToken(token, new Date(new Date().getTime() - 1)) } function errorCodeTriggerRefreshAuth () { @@ -758,8 +757,8 @@ describe('AuthenticationProvider', () => { return { subscribe: (sub) => subscribers.push(sub), - notify: () => subscribers.forEach(sub => sub.onSuccess()), - notifyError: (e) => subscribers.forEach(sub => sub.onError(e)) + onCompleted: (data) => subscribers.forEach(sub => sub.onCompleted(data)), + onError: (e) => subscribers.forEach(sub => sub.onError(e)) } } }) diff --git a/packages/core/src/auth-token-manager.ts b/packages/core/src/auth-token-manager.ts new file mode 100644 index 000000000..4a633c104 --- /dev/null +++ b/packages/core/src/auth-token-manager.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import auth from './auth' +import { AuthToken } from './types' +import { util } from './internal' + +/** + * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. + * @interface + * @since 5.6 + */ +export default class AuthTokenManager { + /** + * Returns a valid token + * + * @returns {Promise|AuthToken} The valid auth token or a promise for a valid auth token + */ + getToken (): Promise | AuthToken { + throw new Error('Not Implemented') + } + + onTokenExpired (token: AuthToken): void { + throw new Error('Not implemented') + } +} + +/** + * Interface which defines an {@link AuthToken} with an expiry data time associated + * @interface + * @since 5.6 + */ +export class TemporalAuthData { + public readonly token: AuthToken + public readonly expiry?: Date + + private constructor () { + /** + * The {@link AuthToken} used for authenticate connections. + * + * @type {AuthToken} + * @see {auth} + */ + this.token = auth.none() as AuthToken + + /** + * The expected expiration date of the auth token. + * + * This information will be used for triggering the auth token refresh + * in managers created with {@link temporalAuthDataManager}. + * + * If this value is not defined, the {@link AuthToken} will be considered valid + * until a `Neo.ClientError.Security.TokenExpired` error happens. + * + * @type {Date|undefined} + */ + this.expiry = undefined + } +} + +/** + * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. + * + * @param {object} param0 - The params + * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token + * @returns {AuthTokenManager} The temporal auth data manager. + */ +export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => Promise }): AuthTokenManager { + if (typeof getAuthData !== 'function') { + throw new TypeError(`getAuthData should be function, but got: ${typeof getAuthData}`) + } + return new TemporalAuthDataManager(getAuthData) +} + +interface TokenRefreshObserver { + onCompleted: (data: TemporalAuthData) => void + onError: (error: Error) => void +} + +class TokenRefreshObservable implements TokenRefreshObserver { + constructor (private readonly _subscribers: TokenRefreshObserver[] = []) { + + } + + subscribe (sub: TokenRefreshObserver): void { + this._subscribers.push(sub) + } + + onCompleted (data: TemporalAuthData): void { + this._subscribers.forEach(sub => sub.onCompleted(data)) + } + + onError (error: Error): void { + this._subscribers.forEach(sub => sub.onError(error)) + } +} + +class TemporalAuthDataManager implements AuthTokenManager { + constructor ( + private readonly _getAuthData: () => Promise, + private _currentAuthData?: TemporalAuthData, + private _refreshObservable?: TokenRefreshObservable) { + + } + + async getToken (): Promise { + if (this._currentAuthData === undefined || + ( + this._currentAuthData.expiry !== undefined && + this._currentAuthData.expiry < new Date() + )) { + await this._refreshAuthToken() + } + + return this._currentAuthData?.token as AuthToken + } + + onTokenExpired (token: AuthToken): void { + if (util.equals(token, this._currentAuthData?.token)) { + this._scheduleRefreshAuthToken() + } + } + + private _scheduleRefreshAuthToken (observer?: TokenRefreshObserver): void { + if (this._refreshObservable === undefined) { + this._currentAuthData = undefined + this._refreshObservable = new TokenRefreshObservable() + + Promise.resolve(this._getAuthData()) + .then(data => { + this._currentAuthData = data + this._refreshObservable?.onCompleted(data) + }) + .catch(error => { + this._refreshObservable?.onError(error) + }) + .finally(() => { + this._refreshObservable = undefined + }) + } + + if (observer !== undefined) { + this._refreshObservable.subscribe(observer) + } + } + + private async _refreshAuthToken (): Promise { + return await new Promise((resolve, reject) => { + this._scheduleRefreshAuthToken({ + onCompleted: resolve, + onError: reject + }) + }) + } +} diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index 502166a0e..f15db8dee 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -53,6 +53,11 @@ const auth = { credentials: base64EncodedToken } }, + none: () => { + return { + scheme: 'none' + } + }, custom: ( principal: string, credentials: string, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5532f83f0..80f2eb209 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -87,6 +87,7 @@ import Session, { TransactionConfig } from './session' import Driver, * as driver from './driver' import auth from './auth' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager' +import AuthTokenManager, { temporalAuthDataManager, TemporalAuthData } from './auth-token-manager' import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver' import * as types from './types' import * as json from './json' @@ -163,6 +164,7 @@ const forExport = { json, auth, bookmarkManager, + temporalAuthDataManager, routing, resultTransformers, notificationCategory, @@ -230,6 +232,7 @@ export { json, auth, bookmarkManager, + temporalAuthDataManager, routing, resultTransformers, notificationCategory, @@ -247,6 +250,8 @@ export type { TransactionConfig, BookmarkManager, BookmarkManagerConfig, + AuthTokenManager, + TemporalAuthData, SessionConfig, QueryConfig, RoutingControl, diff --git a/packages/core/src/internal/util.ts b/packages/core/src/internal/util.ts index 1d7e152d7..903881f19 100644 --- a/packages/core/src/internal/util.ts +++ b/packages/core/src/internal/util.ts @@ -223,6 +223,44 @@ function isString (str: any): str is string { return Object.prototype.toString.call(str) === '[object String]' } +/** + * Verifies if object are the equals + * @param {unknown} a + * @param {unknown} b + * @returns {boolean} + */ +function equals (a: unknown, b: unknown): boolean { + if (a === b) { + return true + } + + if (a === null || b === null) { + return false + } + + if (typeof a === 'object' && typeof b === 'object') { + const keysA = Object.keys(a) + const keysB = Object.keys(b) + + if (keysA.length !== keysB.length) { + return false + } + + type AObjectKey = keyof typeof a + type BObjectKey = keyof typeof b + + for (const key of keysA) { + if (!equals(a[key as AObjectKey], b[key as BObjectKey])) { + return false + } + } + + return true + } + + return false +} + export { isEmptyObjectOrNull, isObject, @@ -233,6 +271,7 @@ export { assertNumberOrInteger, assertValidDate, validateQueryAndParameters, + equals, ENCRYPTION_ON, ENCRYPTION_OFF } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2ad691d27..463dd4713 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -49,14 +49,6 @@ export interface AuthToken { parameters?: Parameters } -export interface RenewableAuthToken { - expectedExpirationTime?: Date - authToken: AuthToken -} - -// Can be async, the user probably wants to do some IO. -export type AuthTokenProvider = () => Promise | RenewableAuthToken - export interface Config { encrypted?: boolean | EncryptionLevel trust?: TrustStrategy diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index 5ee5ada4b..a8f9d99ec 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -17,17 +17,18 @@ * limitations under the License. */ +import { temporalAuthDataManager } from '../../core/index.ts' import { object } from '../lang/index.js' /** * Class which provides Authorization for {@link Connection} */ export default class AuthenticationProvider { - constructor ({ authTokenProvider, userAgent }) { - this._getAuthToken = authTokenProvider || (() => ({})) - this._renewableAuthToken = undefined + constructor ({ authTokenManager, userAgent }) { + this._authTokenManager = authTokenManager || temporalAuthDataManager({ + getAuthData: () => {} + }) this._userAgent = userAgent - this._refreshObserver = undefined } async authenticate ({ connection, auth }) { @@ -38,12 +39,10 @@ export default class AuthenticationProvider { return connection } - if (!this._authToken || this._isTokenExpired) { - await this._getFreshAuthToken() - } + const authToken = await this._authTokenManager.getToken() - if (!object.equals(this._renewableAuthToken.authToken, connection.authToken)) { - return await connection.connect(this._userAgent, this._authToken) + if (!object.equals(authToken, connection.authToken)) { + return await connection.connect(this._userAgent, authToken) } return connection @@ -52,70 +51,12 @@ export default class AuthenticationProvider { handleError ({ connection, code }) { if ( connection && - object.equals(connection.authToken, this._authToken) && [ 'Neo.ClientError.Security.Unauthorized', 'Neo.ClientError.Security.TokenExpired' ].includes(code) ) { - this._scheduleRefresh() - } - } - - get _authToken () { - if (this._renewableAuthToken) { - return this._renewableAuthToken.authToken - } - return undefined - } - - get _isTokenExpired () { - return !this._renewableAuthToken || - (this._renewableAuthToken.expectedExpirationTime && - this._renewableAuthToken.expectedExpirationTime < new Date()) - } - - async _getFreshAuthToken () { - if (this._isTokenExpired) { - const promise = new Promise((resolve, reject) => { - this._scheduleRefresh({ - onSuccess: resolve, - onError: reject - }) - }) - await promise - } - - return this._authToken - } - - _scheduleRefresh (observer) { - // there is no refresh schedule - if (!this._refreshObserver) { - const subscribers = [] - - this._refreshObserver = { - subscribe: (sub) => subscribers.push(sub), - notify: () => subscribers.forEach(sub => sub.onSuccess()), - notifyError: (e) => subscribers.forEach(sub => sub.onError(e)) - } - - Promise.resolve(this._getAuthToken()) - .then(token => { - this._renewableAuthToken = token - this._refreshObserver.notify() - return token - }) - .catch(e => { - this._refreshObserver.notifyError(e) - }) - .finally(() => { - this._refreshObserver = undefined - }) - } - - if (observer) { - this._refreshObserver.subscribe(observer) + this._authTokenManager.onTokenExpired(connection.authToken) } } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 6de4e9fd9..7d84ccd1f 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -37,8 +37,8 @@ const { const { SERVICE_UNAVAILABLE } = error export default class DirectConnectionProvider extends PooledConnectionProvider { - constructor ({ id, config, log, address, userAgent, authTokenProvider, newPool }) { - super({ id, config, log, userAgent, authTokenProvider, newPool }) + constructor ({ id, config, log, address, userAgent, authTokenManager, newPool }) { + super({ id, config, log, userAgent, authTokenManager, newPool }) this._address = address } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index abed26010..a5bf8741b 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -33,7 +33,7 @@ const AUTHENTICATION_ERRORS = [ export default class PooledConnectionProvider extends ConnectionProvider { constructor ( - { id, config, log, userAgent, authTokenProvider, newPool = (...args) => new Pool(...args) }, + { id, config, log, userAgent, authTokenManager, newPool = (...args) => new Pool(...args) }, createChannelConnectionHook = null ) { super() @@ -41,7 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log - this._authenticationProvider = new AuthenticationProvider({ authTokenProvider, userAgent }) + this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent }) this._createChannelConnection = createChannelConnectionHook || (address => { @@ -66,13 +66,27 @@ export default class PooledConnectionProvider extends ConnectionProvider { config: PoolConfig.fromDriverConfig(config), log: this._log }) + this._userAgent = userAgent this._openConnections = {} } async verifyAuthentication ({ auth, database, accessMode, allowStickyConnection } = {}) { try { const connection = await this.acquireConnection({ accessMode, database, auth, allowStickyConnection }) - await connection._release() + const address = connection.address + const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogin() + try { + if (lastMessageIsNotLogin && connection.supportsReAuth) { + await connection.connect(this._userAgent, auth) + } + } finally { + await connection._release() + } + if (lastMessageIsNotLogin && !connection.supportsReAuth) { + const stickyConnection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) + stickyConnection._sticky = true + await stickyConnection._release() + } return true } catch (error) { if (AUTHENTICATION_ERRORS.includes(error.code)) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 55f481b1c..b3317e213 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -67,11 +67,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider config, log, userAgent, - authTokenProvider, + authTokenManager, routingTablePurgeDelay, newPool }) { - super({ id, config, log, userAgent, authTokenProvider, newPool }, address => { + super({ id, config, log, userAgent, authTokenManager, newPool }, address => { return createChannelConnection( address, this._config, diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts new file mode 100644 index 000000000..de0c52e40 --- /dev/null +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import auth from './auth.ts' +import { AuthToken } from './types.ts' +import { util } from './internal/index.ts' + +/** + * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. + * @interface + * @since 5.6 + */ +export default class AuthTokenManager { + /** + * Returns a valid token + * + * @returns {Promise|AuthToken} The valid auth token or a promise for a valid auth token + */ + getToken (): Promise | AuthToken { + throw new Error('Not Implemented') + } + + onTokenExpired (token: AuthToken): void { + throw new Error('Not implemented') + } +} + +/** + * Interface which defines an {@link AuthToken} with an expiry data time associated + * @interface + * @since 5.6 + */ +export class TemporalAuthData { + public readonly token: AuthToken + public readonly expiry?: Date + + private constructor () { + /** + * The {@link AuthToken} used for authenticate connections. + * + * @type {AuthToken} + * @see {auth} + */ + this.token = auth.none() as AuthToken + + /** + * The expected expiration date of the auth token. + * + * This information will be used for triggering the auth token refresh + * in managers created with {@link temporalAuthDataManager}. + * + * If this value is not defined, the {@link AuthToken} will be considered valid + * until a `Neo.ClientError.Security.TokenExpired` error happens. + * + * @type {Date|undefined} + */ + this.expiry = undefined + } +} + +/** + * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. + * + * @param {object} param0 - The params + * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token + * @returns {AuthTokenManager} The temporal auth data manager. + */ +export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => Promise }): AuthTokenManager { + if (typeof getAuthData !== 'function') { + throw new TypeError(`getAuthData should be function, but got: ${typeof getAuthData}`) + } + return new TemporalAuthDataManager(getAuthData) +} + +interface TokenRefreshObserver { + onCompleted: (data: TemporalAuthData) => void + onError: (error: Error) => void +} + +class TokenRefreshObservable implements TokenRefreshObserver { + constructor (private readonly _subscribers: TokenRefreshObserver[] = []) { + + } + + subscribe (sub: TokenRefreshObserver): void { + this._subscribers.push(sub) + } + + onCompleted (data: TemporalAuthData): void { + this._subscribers.forEach(sub => sub.onCompleted(data)) + } + + onError (error: Error): void { + this._subscribers.forEach(sub => sub.onError(error)) + } +} + +class TemporalAuthDataManager implements AuthTokenManager { + constructor ( + private readonly _getAuthData: () => Promise, + private _currentAuthData?: TemporalAuthData, + private _refreshObservable?: TokenRefreshObservable) { + + } + + async getToken (): Promise { + if (this._currentAuthData === undefined || + ( + this._currentAuthData.expiry !== undefined && + this._currentAuthData.expiry < new Date() + )) { + await this._refreshAuthToken() + } + + return this._currentAuthData?.token as AuthToken + } + + onTokenExpired (token: AuthToken): void { + if (util.equals(token, this._currentAuthData?.token)) { + this._scheduleRefreshAuthToken() + } + } + + private _scheduleRefreshAuthToken (observer?: TokenRefreshObserver): void { + if (this._refreshObservable === undefined) { + this._currentAuthData = undefined + this._refreshObservable = new TokenRefreshObservable() + + Promise.resolve(this._getAuthData()) + .then(data => { + this._currentAuthData = data + this._refreshObservable?.onCompleted(data) + }) + .catch(error => { + this._refreshObservable?.onError(error) + }) + .finally(() => { + this._refreshObservable = undefined + }) + } + + if (observer !== undefined) { + this._refreshObservable.subscribe(observer) + } + } + + private async _refreshAuthToken (): Promise { + return await new Promise((resolve, reject) => { + this._scheduleRefreshAuthToken({ + onCompleted: resolve, + onError: reject + }) + }) + } +} diff --git a/packages/neo4j-driver-deno/lib/core/auth.ts b/packages/neo4j-driver-deno/lib/core/auth.ts index 502166a0e..f15db8dee 100644 --- a/packages/neo4j-driver-deno/lib/core/auth.ts +++ b/packages/neo4j-driver-deno/lib/core/auth.ts @@ -53,6 +53,11 @@ const auth = { credentials: base64EncodedToken } }, + none: () => { + return { + scheme: 'none' + } + }, custom: ( principal: string, credentials: string, diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 12352d6fd..2412d7add 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -87,6 +87,7 @@ import Session, { TransactionConfig } from './session.ts' import Driver, * as driver from './driver.ts' import auth from './auth.ts' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager.ts' +import AuthTokenManager, { temporalAuthDataManager, TemporalAuthData } from './auth-token-manager.ts' import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver.ts' import * as types from './types.ts' import * as json from './json.ts' @@ -163,6 +164,7 @@ const forExport = { json, auth, bookmarkManager, + temporalAuthDataManager, routing, resultTransformers, notificationCategory, @@ -230,6 +232,7 @@ export { json, auth, bookmarkManager, + temporalAuthDataManager, routing, resultTransformers, notificationCategory, @@ -247,6 +250,8 @@ export type { TransactionConfig, BookmarkManager, BookmarkManagerConfig, + AuthTokenManager, + TemporalAuthData, SessionConfig, QueryConfig, RoutingControl, diff --git a/packages/neo4j-driver-deno/lib/core/internal/util.ts b/packages/neo4j-driver-deno/lib/core/internal/util.ts index 3a181fdae..d0b691b8e 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/util.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/util.ts @@ -223,6 +223,44 @@ function isString (str: any): str is string { return Object.prototype.toString.call(str) === '[object String]' } +/** + * Verifies if object are the equals + * @param {unknown} a + * @param {unknown} b + * @returns {boolean} + */ +function equals (a: unknown, b: unknown): boolean { + if (a === b) { + return true + } + + if (a === null || b === null) { + return false + } + + if (typeof a === 'object' && typeof b === 'object') { + const keysA = Object.keys(a) + const keysB = Object.keys(b) + + if (keysA.length !== keysB.length) { + return false + } + + type AObjectKey = keyof typeof a + type BObjectKey = keyof typeof b + + for (const key of keysA) { + if (!equals(a[key as AObjectKey], b[key as BObjectKey])) { + return false + } + } + + return true + } + + return false +} + export { isEmptyObjectOrNull, isObject, @@ -233,6 +271,7 @@ export { assertNumberOrInteger, assertValidDate, validateQueryAndParameters, + equals, ENCRYPTION_ON, ENCRYPTION_OFF } diff --git a/packages/neo4j-driver-deno/lib/core/types.ts b/packages/neo4j-driver-deno/lib/core/types.ts index 2ad691d27..463dd4713 100644 --- a/packages/neo4j-driver-deno/lib/core/types.ts +++ b/packages/neo4j-driver-deno/lib/core/types.ts @@ -49,14 +49,6 @@ export interface AuthToken { parameters?: Parameters } -export interface RenewableAuthToken { - expectedExpirationTime?: Date - authToken: AuthToken -} - -// Can be async, the user probably wants to do some IO. -export type AuthTokenProvider = () => Promise | RenewableAuthToken - export interface Config { encrypted?: boolean | EncryptionLevel trust?: TrustStrategy diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 8a32f8491..b4ed0b258 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -93,7 +93,10 @@ import { NotificationFilterDisabledCategory, NotificationFilterMinimumSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + AuthTokenManager, + temporalAuthDataManager, + TemporalAuthData } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { @@ -102,8 +105,6 @@ import { } from './bolt-connection/index.js' type AuthToken = coreTypes.AuthToken -type RenewableAuthToken = coreTypes.RenewableAuthToken -type AuthTokenProvider = coreTypes.AuthTokenProvider type Config = coreTypes.Config type TrustStrategy = coreTypes.TrustStrategy type EncryptionLevel = coreTypes.EncryptionLevel @@ -119,20 +120,36 @@ const { urlUtil } = internal -function createAuthProvider (authTokenOrProvider: AuthToken | AuthTokenProvider): AuthTokenProvider { - if (typeof authTokenOrProvider === 'function') { +function isAuthTokenManager (value: unknown): value is AuthTokenManager { + if (typeof value === 'object' && + value != null && + 'getToken' in value && + 'onTokenExpired' in value) { + const manager = value as AuthTokenManager + + return typeof manager.getToken === 'function' && + typeof manager.onTokenExpired === 'function' + } + + return false +} + +function createAuthManager (authTokenOrProvider: AuthToken | AuthTokenManager): AuthTokenManager { + if (isAuthTokenManager(authTokenOrProvider)) { return authTokenOrProvider } - let authToken: AuthToken = authTokenOrProvider + let token: AuthToken = authTokenOrProvider // Sanitize authority token. Nicer error from server when a scheme is set. - authToken = authToken ?? {} - authToken.scheme = authToken.scheme ?? 'none' - return function (): RenewableAuthToken { - return { - authToken + token = token ?? {} + token.scheme = token.scheme ?? 'none' + return temporalAuthDataManager({ + getAuthData: async function (): Promise { + return { + token + } } - } + }) } /** @@ -271,7 +288,7 @@ function createAuthProvider (authTokenOrProvider: AuthToken | AuthTokenProvider) */ function driver ( url: string, - authToken: AuthToken | AuthTokenProvider, + authToken: AuthToken | AuthTokenManager, config: Config = {} ): Driver { assertString(url, 'Bolt URL') @@ -321,7 +338,7 @@ function driver ( config.trust = trust } - const authTokenProvider = createAuthProvider(authToken) + const authTokenManager = createAuthManager(authToken) // Use default user agent or user agent specified by user. config.userAgent = config.userAgent ?? USER_AGENT @@ -348,7 +365,7 @@ function driver ( config, log, hostNameResolver, - authTokenProvider, + authTokenManager, address, userAgent: config.userAgent, routingContext: parsedUrl.query @@ -365,7 +382,7 @@ function driver ( id, config, log, - authTokenProvider, + authTokenManager, address, userAgent: config.userAgent }) @@ -531,7 +548,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + temporalAuthDataManager } export { @@ -597,13 +615,14 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + temporalAuthDataManager } export type { QueryResult, AuthToken, - AuthTokenProvider, - RenewableAuthToken, + AuthTokenManager, + TemporalAuthData, Config, EncryptionLevel, TrustStrategy, diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index b5218a3b1..cc297cabb 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -93,7 +93,10 @@ import { NotificationFilterDisabledCategory, NotificationFilterMinimumSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + AuthTokenManager, + temporalAuthDataManager, + TemporalAuthData } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -101,8 +104,6 @@ import { } from 'neo4j-driver-bolt-connection' type AuthToken = coreTypes.AuthToken -type RenewableAuthToken = coreTypes.RenewableAuthToken -type AuthTokenProvider = coreTypes.AuthTokenProvider type Config = coreTypes.Config type TrustStrategy = coreTypes.TrustStrategy type EncryptionLevel = coreTypes.EncryptionLevel @@ -118,20 +119,36 @@ const { urlUtil } = internal -function createAuthProvider (authTokenOrProvider: AuthToken | AuthTokenProvider): AuthTokenProvider { - if (typeof authTokenOrProvider === 'function') { +function isAuthTokenManager (value: unknown): value is AuthTokenManager { + if (typeof value === 'object' && + value != null && + 'getToken' in value && + 'onTokenExpired' in value) { + const manager = value as AuthTokenManager + + return typeof manager.getToken === 'function' && + typeof manager.onTokenExpired === 'function' + } + + return false +} + +function createAuthManager (authTokenOrProvider: AuthToken | AuthTokenManager): AuthTokenManager { + if (isAuthTokenManager(authTokenOrProvider)) { return authTokenOrProvider } - let authToken: AuthToken = authTokenOrProvider + let token: AuthToken = authTokenOrProvider // Sanitize authority token. Nicer error from server when a scheme is set. - authToken = authToken ?? {} - authToken.scheme = authToken.scheme ?? 'none' - return function (): RenewableAuthToken { - return { - authToken + token = token ?? {} + token.scheme = token.scheme ?? 'none' + return temporalAuthDataManager({ + getAuthData: async function (): Promise { + return { + token + } } - } + }) } /** @@ -270,7 +287,7 @@ function createAuthProvider (authTokenOrProvider: AuthToken | AuthTokenProvider) */ function driver ( url: string, - authToken: AuthToken | AuthTokenProvider, + authToken: AuthToken | AuthTokenManager, config: Config = {} ): Driver { assertString(url, 'Bolt URL') @@ -320,7 +337,7 @@ function driver ( config.trust = trust } - const authTokenProvider = createAuthProvider(authToken) + const authTokenManager = createAuthManager(authToken) // Use default user agent or user agent specified by user. config.userAgent = config.userAgent ?? USER_AGENT @@ -347,7 +364,7 @@ function driver ( config, log, hostNameResolver, - authTokenProvider, + authTokenManager, address, userAgent: config.userAgent, routingContext: parsedUrl.query @@ -364,7 +381,7 @@ function driver ( id, config, log, - authTokenProvider, + authTokenManager, address, userAgent: config.userAgent }) @@ -530,7 +547,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + temporalAuthDataManager } export { @@ -596,13 +614,14 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + temporalAuthDataManager } export type { QueryResult, AuthToken, - AuthTokenProvider, - RenewableAuthToken, + AuthTokenManager, + TemporalAuthData, Config, EncryptionLevel, TrustStrategy, diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index d2261e1c9..34a7e02bb 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -73,7 +73,8 @@ import { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + temporalAuthDataManager } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -91,20 +92,31 @@ const { urlUtil } = internal -function createAuthProvider (authTokenOrProvider) { - if (typeof authTokenOrProvider === 'function') { - return authTokenOrProvider +function isAuthTokenManager (value) { + return typeof value === 'object' && + value != null && + 'getToken' in value && + 'onTokenExpired' in value && + typeof value.getToken === 'function' && + typeof value.onTokenExpired === 'function' +} + +function createAuthManager (authTokenOrManager) { + if (isAuthTokenManager(authTokenOrManager)) { + return authTokenOrManager } - let authToken = authTokenOrProvider + let token = authTokenOrManager // Sanitize authority token. Nicer error from server when a scheme is set. - authToken = authToken ?? {} - authToken.scheme = authToken.scheme ?? 'none' - return function () { - return { - authToken + token = token || {} + token.scheme = token.scheme || 'none' + return temporalAuthDataManager({ + getAuthData: async function () { + return { + token + } } - } + }) } /** @@ -289,7 +301,7 @@ function driver (url, authToken, config = {}) { config.trust = trust } - const authTokenProvider = createAuthProvider(authToken) + const authTokenManager = createAuthManager(authToken) // Use default user agent or user agent specified by user. config.userAgent = config.userAgent || USER_AGENT @@ -311,7 +323,7 @@ function driver (url, authToken, config = {}) { config, log, hostNameResolver, - authTokenProvider, + authTokenManager, address, userAgent: config.userAgent, routingContext: parsedUrl.query @@ -328,7 +340,7 @@ function driver (url, authToken, config = {}) { id, config, log, - authTokenProvider, + authTokenManager, address, userAgent: config.userAgent }) @@ -510,7 +522,8 @@ const forExport = { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + temporalAuthDataManager } export { @@ -577,6 +590,7 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + temporalAuthDataManager } export default forExport diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index e0dfa1865..3963c481a 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -83,7 +83,10 @@ import { NotificationFilterDisabledCategory, NotificationFilterMinimumSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + AuthTokenManager, + TemporalAuthData, + temporalAuthDataManager } from 'neo4j-driver-core' import { AuthToken, @@ -265,6 +268,7 @@ declare const forExport: { notificationSeverityLevel: typeof notificationSeverityLevel notificationFilterDisabledCategory: typeof notificationFilterDisabledCategory notificationFilterMinimumSeverityLevel: typeof notificationFilterMinimumSeverityLevel + temporalAuthDataManager: typeof temporalAuthDataManager } export { @@ -338,7 +342,8 @@ export { notificationCategory, notificationSeverityLevel, notificationFilterDisabledCategory, - notificationFilterMinimumSeverityLevel + notificationFilterMinimumSeverityLevel, + temporalAuthDataManager } export type { @@ -352,7 +357,9 @@ export type { NotificationSeverityLevel, NotificationFilter, NotificationFilterDisabledCategory, - NotificationFilterMinimumSeverityLevel + NotificationFilterMinimumSeverityLevel, + AuthTokenManager, + TemporalAuthData } export default forExport diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index ba1f7fd5f..0a709e6c1 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -26,7 +26,9 @@ export function NewDriver ({ neo4j }, context, data, wire) { const { data: authToken } = authorizationToken parsedAuthToken = context.binder.parseAuthToken(authToken) } else { - parsedAuthToken = context.getAuthTokenProvider(authTokenProviderId) + parsedAuthToken = neo4j.temporalAuthDataManager({ + getAuthData: context.getAuthTokenProvider(authTokenProviderId) + }) } const resolver = resolverRegistered From ed4a56d7830656c5db349e97a9d5e68fa9d69459 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 21 Feb 2023 14:35:13 +0100 Subject: [PATCH 46/70] Adjust testkit --- packages/testkit-backend/src/request-handlers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 0a709e6c1..5b60886c7 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -542,10 +542,10 @@ export function NewAuthTokenProvider (_, context, _data, wire) { export function AuthTokenProviderCompleted (_, context, { requestId, auth }, _wire) { const request = context.getAuthTokenProviderRequest(requestId) const renewableToken = { - expectedExpirationTime: auth.data.expiresInMs != null + expiry: auth.data.expiresInMs != null ? new Date(new Date().getTime() + auth.data.expiresInMs) : undefined, - authToken: context.binder.parseAuthToken(auth.data.auth.data) + token: context.binder.parseAuthToken(auth.data.auth.data) } request.resolve(renewableToken) context.removeAuthTokenProviderRequest(requestId) From 19a332c9bde629d5dda0dcb13b81daa2e7bce93b Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 22 Feb 2023 14:17:04 +0100 Subject: [PATCH 47/70] Release sticky connection and only drop connection when needed --- packages/bolt-connection/src/pool/pool.js | 2 +- packages/core/src/internal/connection-holder.ts | 4 ---- packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js | 2 +- .../neo4j-driver-deno/lib/core/internal/connection-holder.ts | 4 ---- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/bolt-connection/src/pool/pool.js b/packages/bolt-connection/src/pool/pool.js index 41398a281..685140260 100644 --- a/packages/bolt-connection/src/pool/pool.js +++ b/packages/bolt-connection/src/pool/pool.js @@ -247,7 +247,7 @@ class Pool { this._pendingCreates[key] = this._pendingCreates[key] + 1 let resource try { - if (pool.length && requireNew) { + if (pool.length >= this._maxSize && requireNew) { const resource = pool.pop() if (this._removeIdleObserver) { this._removeIdleObserver(resource) diff --git a/packages/core/src/internal/connection-holder.ts b/packages/core/src/internal/connection-holder.ts index 652fade05..3ba2eafe6 100644 --- a/packages/core/src/internal/connection-holder.ts +++ b/packages/core/src/internal/connection-holder.ts @@ -232,10 +232,6 @@ class ConnectionHolder implements ConnectionHolderInterface { .catch(ignoreError) .then(() => connection._release().then(() => null)) } - - if (!this._closed && (this._auth != null) && !connection.supportsReAuth) { - return connection - } return connection._release().then(() => null) } else { return Promise.resolve(null) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js index c5fe6c769..0984988eb 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js @@ -247,7 +247,7 @@ class Pool { this._pendingCreates[key] = this._pendingCreates[key] + 1 let resource try { - if (pool.length && requireNew) { + if (pool.length >= this._maxSize && requireNew) { const resource = pool.pop() if (this._removeIdleObserver) { this._removeIdleObserver(resource) diff --git a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts index bb285163b..967266ca2 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts @@ -232,10 +232,6 @@ class ConnectionHolder implements ConnectionHolderInterface { .catch(ignoreError) .then(() => connection._release().then(() => null)) } - - if (!this._closed && (this._auth != null) && !connection.supportsReAuth) { - return connection - } return connection._release().then(() => null) } else { return Promise.resolve(null) From 2350c7c78c40218706408bb09c2d477aa31c6d17 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 22 Feb 2023 15:05:05 +0100 Subject: [PATCH 48/70] Fix pool size comparison --- packages/bolt-connection/src/pool/pool.js | 3 ++- packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bolt-connection/src/pool/pool.js b/packages/bolt-connection/src/pool/pool.js index 685140260..187393f5f 100644 --- a/packages/bolt-connection/src/pool/pool.js +++ b/packages/bolt-connection/src/pool/pool.js @@ -247,7 +247,8 @@ class Pool { this._pendingCreates[key] = this._pendingCreates[key] + 1 let resource try { - if (pool.length >= this._maxSize && requireNew) { + const numConnections = this.activeResourceCount(address) + pool.length + if (numConnections >= this._maxSize && requireNew) { const resource = pool.pop() if (this._removeIdleObserver) { this._removeIdleObserver(resource) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js index 0984988eb..c1789564c 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/pool/pool.js @@ -247,7 +247,8 @@ class Pool { this._pendingCreates[key] = this._pendingCreates[key] + 1 let resource try { - if (pool.length >= this._maxSize && requireNew) { + const numConnections = this.activeResourceCount(address) + pool.length + if (numConnections >= this._maxSize && requireNew) { const resource = pool.pop() if (this._removeIdleObserver) { this._removeIdleObserver(resource) From 97f59cdd76ec998ec45bb12054e50baec3b66e53 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Thu, 23 Feb 2023 17:38:56 +0100 Subject: [PATCH 49/70] VerifyAuthentication ajusts The auth validation when the connection is not new should be done in the provider. --- .../authentication-provider.js | 10 +++- .../connection-provider-direct.js | 12 +++- .../connection-provider-pooled.js | 60 ++++++++++--------- .../connection-provider-routing.js | 31 ++++++++++ .../src/connection/connection-channel.js | 19 +++++- .../src/connection/connection-delegate.js | 4 +- .../authorization-provider.test.js | 4 +- .../connection/connection-channel.test.js | 2 +- .../authentication-provider.js | 10 +++- .../connection-provider-direct.js | 12 +++- .../connection-provider-pooled.js | 60 ++++++++++--------- .../connection-provider-routing.js | 31 ++++++++++ .../connection/connection-channel.js | 19 +++++- .../connection/connection-delegate.js | 4 +- 14 files changed, 201 insertions(+), 77 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index 38adf36c3..631f4aecb 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -31,10 +31,14 @@ export default class AuthenticationProvider { this._userAgent = userAgent } - async authenticate ({ connection, auth }) { + async authenticate ({ connection, auth, skipReAuth, waitReAuth, forceReAuth }) { if (auth != null) { - if (connection.authToken == null || (connection.supportsReAuth && !object.equals(connection.authToken, auth))) { - return await connection.connect(this._userAgent, auth) + const shouldReAuth = connection.supportsReAuth === true && ( + (!object.equals(connection.authToken, auth) && skipReAuth !== true) || + forceReAuth === true + ) + if (connection.authToken == null || shouldReAuth) { + return await connection.connect(this._userAgent, auth, waitReAuth || false) } return connection } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 752fa144e..e30012131 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -47,14 +47,14 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, auth, allowStickyConnection } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, auth, allowStickyConnection, forceReAuth } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn, database) }) - const connection = await this._connectionPool.acquire({ auth }, this._address) + const connection = await this._connectionPool.acquire({ auth, forceReAuth }, this._address) if (auth) { const stickyConnection = await this._getStickyConnection({ @@ -139,6 +139,14 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } + async verifyAuthentication ({ auth, allowStickyConnection }) { + return this._verifyAuthentication({ + allowStickyConnection, + auth, + getAddress: () => this._address + }) + } + async verifyConnectivityAndGetServerInfo () { return await this._verifyConnectivityAndGetServerVersion({ address: this._address }) } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index ec38c4634..23e3c7a9d 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -70,32 +70,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._openConnections = {} } - async verifyAuthentication ({ auth, database, accessMode, allowStickyConnection } = {}) { - try { - const connection = await this.acquireConnection({ accessMode, database, auth, allowStickyConnection }) - const address = connection.address - const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogin() - try { - if (lastMessageIsNotLogin && connection.supportsReAuth) { - await connection.connect(this._userAgent, auth) - } - } finally { - await connection._release() - } - if (lastMessageIsNotLogin && !connection.supportsReAuth) { - const stickyConnection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) - stickyConnection._sticky = true - await stickyConnection._release() - } - return true - } catch (error) { - if (AUTHENTICATION_ERRORS.includes(error.code)) { - return false - } - throw error - } - } - _createConnectionErrorHandler () { return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) } @@ -121,13 +95,13 @@ export default class PooledConnectionProvider extends ConnectionProvider { }) } - async _validateConnectionOnAcquire ({ auth }, conn) { + async _validateConnectionOnAcquire ({ auth, skipReAuth }, conn) { if (!this._validateConnection(conn)) { return false } try { - await this._authenticationProvider.authenticate({ connection: conn, auth }) + await this._authenticationProvider.authenticate({ connection: conn, auth, skipReAuth }) return true } catch (error) { this._log.debug( @@ -189,6 +163,36 @@ export default class PooledConnectionProvider extends ConnectionProvider { return serverInfo } + async _verifyAuthentication ({ getAddress, auth, allowStickyConnection }) { + const connectionsToRelease = [] + try { + const address = await getAddress() + const connection = await this._connectionPool.acquire({ auth, skipReAuth: true }, address) + connectionsToRelease.push(connection) + + const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogin() + + if (!connection.supportsReAuth && !allowStickyConnection) { + throw newError('Driver is connected to a database that does not support user switch.') + } + if (lastMessageIsNotLogin && connection.supportsReAuth) { + await this._authenticationProvider.authenticate({ connection, auth, waitReAuth: true, forceReAuth: true }) + } else if (lastMessageIsNotLogin && !connection.supportsReAuth) { + const stickyConnection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) + stickyConnection._sticky = true + connectionsToRelease.push(stickyConnection) + } + return true + } catch (error) { + if (AUTHENTICATION_ERRORS.includes(error.code)) { + return false + } + throw error + } finally { + await Promise.all(connectionsToRelease.map(conn => conn._release())) + } + } + async _getStickyConnection ({ auth, connection, address, allowStickyConnection }) { const connectionWithSameCredentials = object.equals(auth, connection.authToken) const shouldCreateStickyConnection = !connectionWithSameCredentials diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 33786c31b..a157a98b0 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -284,6 +284,37 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } + async verifyAuthentication ({ database, accessMode, auth, allowStickyConnection }) { + return this._verifyAuthentication({ + allowStickyConnection, + auth, + getAddress: async () => { + const context = { database: database || DEFAULT_DB_NAME } + + const routingTable = await this._freshRoutingTable({ + accessMode, + database: context.database, + auth, + allowStickyConnection, + onDatabaseNameResolved: (databaseName) => { + context.database = context.database || databaseName + } + }) + + const servers = accessMode === WRITE ? routingTable.writers : routingTable.readers + + if (servers.length === 0) { + throw newError( + `No servers available for database '${context.database}' with access mode '${accessMode}'`, + SERVICE_UNAVAILABLE + ) + } + + return servers[0] + } + }) + } + async verifyConnectivityAndGetServerInfo ({ database, accessMode }) { const context = { database: database || DEFAULT_DB_NAME } diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index dda84cfe9..5f743f987 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -186,7 +186,7 @@ export default class ChannelConnection extends Connection { * @param {Object} authToken the object containing auth information. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ - async connect (userAgent, authToken) { + async connect (userAgent, authToken, waitReAuth) { if (this._protocol.initialized && !this._protocol.supportsLogoff) { throw newError('Connection does not support re-auth') } @@ -197,8 +197,23 @@ export default class ChannelConnection extends Connection { return await this._initialize(userAgent, authToken) } + if (waitReAuth) { + return await new Promise((resolve, reject) => { + this._protocol.logoff({ + onError: reject + }) + + this._protocol.login({ + authToken, + onError: reject, + onComplete: () => resolve(this), + flush: true + }) + }) + } + this._protocol.logoff() - this._protocol.login({ authToken }) + this._protocol.login({ authToken, flush: true }) return this } diff --git a/packages/bolt-connection/src/connection/connection-delegate.js b/packages/bolt-connection/src/connection/connection-delegate.js index fd8d0aa03..1f77823f7 100644 --- a/packages/bolt-connection/src/connection/connection-delegate.js +++ b/packages/bolt-connection/src/connection/connection-delegate.js @@ -83,8 +83,8 @@ export default class DelegateConnection extends Connection { return this._delegate.protocol() } - connect (userAgent, authToken) { - return this._delegate.connect(userAgent, authToken) + connect (userAgent, authToken, waitReAuth) { + return this._delegate.connect(userAgent, authToken, waitReAuth) } write (message, observer, flush) { diff --git a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js index c73d96816..360dbf184 100644 --- a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authorization-provider.test.js @@ -405,7 +405,7 @@ describe('AuthenticationProvider', () => { await authenticationProvider.authenticate({ connection, auth }) - expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, auth) + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, auth, false) }) it('should return the connection', async () => { @@ -484,7 +484,7 @@ describe('AuthenticationProvider', () => { await authenticationProvider.authenticate({ connection, auth }) - expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, auth) + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, auth, false) }) it('should return the connection', async () => { diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 196604a80..49b788597 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -185,7 +185,7 @@ describe('ChannelConnection', () => { expect(protocol.initialize).not.toHaveBeenCalled() expect(protocol.logoff).toHaveBeenCalledWith() - expect(protocol.login).toHaveBeenCalledWith({ authToken }) + expect(protocol.login).toHaveBeenCalledWith({ authToken, flush: true }) expect(connection.authToken).toEqual(authToken) }) }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index a8f9d99ec..f04993bb6 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -31,10 +31,14 @@ export default class AuthenticationProvider { this._userAgent = userAgent } - async authenticate ({ connection, auth }) { + async authenticate ({ connection, auth, skipReAuth, waitReAuth, forceReAuth }) { if (auth != null) { - if (connection.authToken == null || (connection.supportsReAuth && !object.equals(connection.authToken, auth))) { - return await connection.connect(this._userAgent, auth) + const shouldReAuth = connection.supportsReAuth === true && ( + (!object.equals(connection.authToken, auth) && skipReAuth !== true) || + forceReAuth === true + ) + if (connection.authToken == null || shouldReAuth) { + return await connection.connect(this._userAgent, auth, waitReAuth || false) } return connection } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 7d84ccd1f..684ae0cea 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -47,14 +47,14 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, auth, allowStickyConnection } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, auth, allowStickyConnection, forceReAuth } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, handleAuthorizationExpired: (error, address, conn) => this._handleAuthorizationExpired(error, address, conn, database) }) - const connection = await this._connectionPool.acquire({ auth }, this._address) + const connection = await this._connectionPool.acquire({ auth, forceReAuth }, this._address) if (auth) { const stickyConnection = await this._getStickyConnection({ @@ -139,6 +139,14 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } + async verifyAuthentication ({ auth, allowStickyConnection }) { + return this._verifyAuthentication({ + allowStickyConnection, + auth, + getAddress: () => this._address + }) + } + async verifyConnectivityAndGetServerInfo () { return await this._verifyConnectivityAndGetServerVersion({ address: this._address }) } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index a5bf8741b..7f930e6e1 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -70,32 +70,6 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._openConnections = {} } - async verifyAuthentication ({ auth, database, accessMode, allowStickyConnection } = {}) { - try { - const connection = await this.acquireConnection({ accessMode, database, auth, allowStickyConnection }) - const address = connection.address - const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogin() - try { - if (lastMessageIsNotLogin && connection.supportsReAuth) { - await connection.connect(this._userAgent, auth) - } - } finally { - await connection._release() - } - if (lastMessageIsNotLogin && !connection.supportsReAuth) { - const stickyConnection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) - stickyConnection._sticky = true - await stickyConnection._release() - } - return true - } catch (error) { - if (AUTHENTICATION_ERRORS.includes(error.code)) { - return false - } - throw error - } - } - _createConnectionErrorHandler () { return new ConnectionErrorHandler(SERVICE_UNAVAILABLE) } @@ -121,13 +95,13 @@ export default class PooledConnectionProvider extends ConnectionProvider { }) } - async _validateConnectionOnAcquire ({ auth }, conn) { + async _validateConnectionOnAcquire ({ auth, skipReAuth }, conn) { if (!this._validateConnection(conn)) { return false } try { - await this._authenticationProvider.authenticate({ connection: conn, auth }) + await this._authenticationProvider.authenticate({ connection: conn, auth, skipReAuth }) return true } catch (error) { this._log.debug( @@ -189,6 +163,36 @@ export default class PooledConnectionProvider extends ConnectionProvider { return serverInfo } + async _verifyAuthentication ({ getAddress, auth, allowStickyConnection }) { + const connectionsToRelease = [] + try { + const address = await getAddress() + const connection = await this._connectionPool.acquire({ auth, skipReAuth: true }, address) + connectionsToRelease.push(connection) + + const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogin() + + if (!connection.supportsReAuth && !allowStickyConnection) { + throw newError('Driver is connected to a database that does not support user switch.') + } + if (lastMessageIsNotLogin && connection.supportsReAuth) { + await this._authenticationProvider.authenticate({ connection, auth, waitReAuth: true, forceReAuth: true }) + } else if (lastMessageIsNotLogin && !connection.supportsReAuth) { + const stickyConnection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) + stickyConnection._sticky = true + connectionsToRelease.push(stickyConnection) + } + return true + } catch (error) { + if (AUTHENTICATION_ERRORS.includes(error.code)) { + return false + } + throw error + } finally { + await Promise.all(connectionsToRelease.map(conn => conn._release())) + } + } + async _getStickyConnection ({ auth, connection, address, allowStickyConnection }) { const connectionWithSameCredentials = object.equals(auth, connection.authToken) const shouldCreateStickyConnection = !connectionWithSameCredentials diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index b3317e213..83518e1b4 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -284,6 +284,37 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } + async verifyAuthentication ({ database, accessMode, auth, allowStickyConnection }) { + return this._verifyAuthentication({ + allowStickyConnection, + auth, + getAddress: async () => { + const context = { database: database || DEFAULT_DB_NAME } + + const routingTable = await this._freshRoutingTable({ + accessMode, + database: context.database, + auth, + allowStickyConnection, + onDatabaseNameResolved: (databaseName) => { + context.database = context.database || databaseName + } + }) + + const servers = accessMode === WRITE ? routingTable.writers : routingTable.readers + + if (servers.length === 0) { + throw newError( + `No servers available for database '${context.database}' with access mode '${accessMode}'`, + SERVICE_UNAVAILABLE + ) + } + + return servers[0] + } + }) + } + async verifyConnectivityAndGetServerInfo ({ database, accessMode }) { const context = { database: database || DEFAULT_DB_NAME } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index 3cc39b712..b090f510a 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -186,7 +186,7 @@ export default class ChannelConnection extends Connection { * @param {Object} authToken the object containing auth information. * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ - async connect (userAgent, authToken) { + async connect (userAgent, authToken, waitReAuth) { if (this._protocol.initialized && !this._protocol.supportsLogoff) { throw newError('Connection does not support re-auth') } @@ -197,8 +197,23 @@ export default class ChannelConnection extends Connection { return await this._initialize(userAgent, authToken) } + if (waitReAuth) { + return await new Promise((resolve, reject) => { + this._protocol.logoff({ + onError: reject + }) + + this._protocol.login({ + authToken, + onError: reject, + onComplete: () => resolve(this), + flush: true + }) + }) + } + this._protocol.logoff() - this._protocol.login({ authToken }) + this._protocol.login({ authToken, flush: true }) return this } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js index 57813ac38..70b43b2fa 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-delegate.js @@ -83,8 +83,8 @@ export default class DelegateConnection extends Connection { return this._delegate.protocol() } - connect (userAgent, authToken) { - return this._delegate.connect(userAgent, authToken) + connect (userAgent, authToken, waitReAuth) { + return this._delegate.connect(userAgent, authToken, waitReAuth) } write (message, observer, flush) { From 18187b91bfcd7f0541b0a1da9c489182f22bd257 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 27 Feb 2023 15:46:44 +0100 Subject: [PATCH 50/70] Clean and test --- ...est.js => authentication-provider.test.js} | 73 ++++++++++++ .../connection-provider-direct.test.js | 18 +++ .../connection-provider-routing.test.js | 18 +++ .../connection/connection-channel.test.js | 108 ++++++++++++++++++ packages/core/src/auth-token-manager.ts | 4 +- 5 files changed, 219 insertions(+), 2 deletions(-) rename packages/bolt-connection/test/connection-provider/{authorization-provider.test.js => authentication-provider.test.js} (89%) diff --git a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js similarity index 89% rename from packages/bolt-connection/test/connection-provider/authorization-provider.test.js rename to packages/bolt-connection/test/connection-provider/authentication-provider.test.js index 360dbf184..ab7538968 100644 --- a/packages/bolt-connection/test/connection-provider/authorization-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js @@ -454,6 +454,41 @@ describe('AuthenticationProvider', () => { expect(connection.connect).not.toHaveBeenCalledWith(USER_AGENT, auth) }) + it('should not call connection connect with the supplied auth and skipReAuth=true', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth } }) + + await authenticationProvider.authenticate({ connection, auth, skipReAuth: true }) + + expect(connection.connect).not.toHaveBeenCalledWith(USER_AGENT, auth) + }) + + if (supportsReAuth) { + it('should call connection connect with the supplied auth if forceReAuth=true', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { scheme: 'bearer', credentials: 'other' } }) + + await authenticationProvider.authenticate({ connection, auth, forceReAuth: true }) + + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, auth, false) + }) + } else { + it('should not call connection connect with the supplied auth if forceReAuth=true', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth } }) + + await authenticationProvider.authenticate({ connection, auth, forceReAuth: true }) + + expect(connection.connect).not.toHaveBeenCalledWith(USER_AGENT, auth) + }) + } + it('should return the connection', async () => { const auth = { scheme: 'bearer', credentials: 'my token' } const authTokenProvider = jest.fn(() => toRenewableToken({})) @@ -520,6 +555,33 @@ describe('AuthenticationProvider', () => { await expect(authenticationProvider.authenticate({ connection, auth })).rejects.toThrow(error) }) + + it.each([ + [true, true], + [false, false], + [undefined, false], + [null, false] + ])('should redirect `waitReAuth=%s` as `%s` to the connection.connect()', async (waitReAuth, expectedWaitForReAuth) => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { scheme: 'bearer', credentials: 'other' } }) + + await authenticationProvider.authenticate({ connection, auth, waitReAuth }) + + expect(connection.connect).toHaveBeenCalledWith(USER_AGENT, auth, expectedWaitForReAuth) + }) + + it('should not call connect when skipReAuth=true', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { scheme: 'bearer', credentials: 'other' } }) + + await authenticationProvider.authenticate({ connection, auth, skipReAuth: true }) + + expect(connection.connect).not.toBeCalled() + }) }) describe.each([false])('and connection is authenticated with different token (supportsReAuth=%s)', (supportsReAuth) => { @@ -534,6 +596,17 @@ describe('AuthenticationProvider', () => { expect(connection.connect).not.toHaveBeenCalledWith(USER_AGENT, auth) }) + it('should not call connection connect with the supplied auth and forceReAuth=true', async () => { + const auth = { scheme: 'bearer', credentials: 'my token' } + const authTokenProvider = jest.fn(() => toRenewableToken({})) + const authenticationProvider = createAuthenticationProvider(authTokenProvider) + const connection = mockConnection({ supportsReAuth, authToken: { ...auth, credentials: 'other' } }) + + await authenticationProvider.authenticate({ connection, auth, forceReAuth: true }) + + expect(connection.connect).not.toHaveBeenCalledWith(USER_AGENT, auth) + }) + it('should return the connection', async () => { const auth = { scheme: 'bearer', credentials: 'my token' } const authTokenProvider = jest.fn(() => toRenewableToken({})) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 41354cb19..9636f194f 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -334,6 +334,24 @@ describe('constructor', () => { `The connection ${connection.id} is not valid because of an error ${error.code} '${error.message}'` ) }) + + it.each([ + true, + false + ])('should call authenticationProvider.authenticate with skipReAuth=%s', async (skipReAuth) => { + const connection = new FakeConnection(server0) + const auth = {} + connection.creationTimestamp = Date.now() + + const { validateOnAcquire, authenticationProviderHook } = setup() + + await expect(validateOnAcquire({ auth, skipReAuth }, connection)).resolves.toBe(true) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, auth, skipReAuth + }) + }) + it('should return false when connection is closed and within the lifetime', async () => { const connection = new FakeConnection(server0) connection.creationTimestamp = Date.now() diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 173ff31ef..66a2dd28c 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3174,6 +3174,24 @@ describe.each([ `The connection ${connection.id} is not valid because of an error ${error.code} '${error.message}'` ) }) + + it.each([ + true, + false + ])('should call authenticationProvider.authenticate with skipReAuth=%s', async (skipReAuth) => { + const connection = new FakeConnection(server0) + const auth = {} + connection.creationTimestamp = Date.now() + + const { validateOnAcquire, authenticationProviderHook } = setup() + + await expect(validateOnAcquire({ auth, skipReAuth }, connection)).resolves.toBe(true) + + expect(authenticationProviderHook.authenticate).toHaveBeenCalledWith({ + connection, auth, skipReAuth + }) + }) + it('should return false when connection is closed and within the lifetime', async () => { const connection = new FakeConnection(server0) connection.creationTimestamp = Date.now() diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 49b788597..bdf61f04d 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -188,6 +188,114 @@ describe('ChannelConnection', () => { expect(protocol.login).toHaveBeenCalledWith({ authToken, flush: true }) expect(connection.authToken).toEqual(authToken) }) + + describe('when waitReAuth=true', () => { + it('should wait for login complete', async () => { + const authToken = { + scheme: 'none' + } + + const onCompleteObservers = [] + const protocol = { + initialize: jest.fn(observer => observer.onComplete({})), + logoff: jest.fn(() => undefined), + login: jest.fn(({ onComplete }) => onCompleteObservers.push(onComplete)), + initialized: true, + supportsLogoff: true + } + + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier }) + + const connectionPromise = connection.connect('userAgent', authToken, true) + + const isPending = await Promise.race([connectionPromise, Promise.resolve(true)]) + expect(isPending).toEqual(true) + expect(onCompleteObservers.length).toEqual(1) + + expect(protocol.initialize).not.toHaveBeenCalled() + expect(protocol.logoff).toHaveBeenCalled() + expect(protocol.login).toHaveBeenCalledWith(expect.objectContaining({ + authToken, + flush: true + })) + + expect(connection.authToken).toEqual(authToken) + + onCompleteObservers.forEach(onComplete => onComplete({})) + await expect(connectionPromise).resolves.toBe(connection) + }) + + it('should notify logoff errors', async () => { + const authToken = { + scheme: 'none' + } + + const onLogoffErrors = [] + const protocol = { + initialize: jest.fn(observer => observer.onComplete({})), + logoff: jest.fn(({ onError }) => onLogoffErrors.push(onError)), + login: jest.fn(() => undefined), + initialized: true, + supportsLogoff: true + } + + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier }) + + const connectionPromise = connection.connect('userAgent', authToken, true) + + const isPending = await Promise.race([connectionPromise, Promise.resolve(true)]) + expect(isPending).toEqual(true) + expect(onLogoffErrors.length).toEqual(1) + + expect(protocol.initialize).not.toHaveBeenCalled() + expect(protocol.logoff).toHaveBeenCalled() + expect(protocol.login).toHaveBeenCalledWith(expect.objectContaining({ + authToken, + flush: true + })) + + const expectedError = newError('something wrong is not right.') + onLogoffErrors.forEach(onError => onError(expectedError)) + await expect(connectionPromise).rejects.toBe(expectedError) + }) + + it('should notify login errors', async () => { + const authToken = { + scheme: 'none' + } + + const onLoginErrors = [] + const protocol = { + initialize: jest.fn(observer => observer.onComplete({})), + logoff: jest.fn(() => undefined), + login: jest.fn(({ onError }) => onLoginErrors.push(onError)), + initialized: true, + supportsLogoff: true + } + + const protocolSupplier = () => protocol + const connection = spyOnConnectionChannel({ protocolSupplier }) + + const connectionPromise = connection.connect('userAgent', authToken, true) + + const isPending = await Promise.race([connectionPromise, Promise.resolve(true)]) + expect(isPending).toEqual(true) + expect(onLoginErrors.length).toEqual(1) + + expect(protocol.initialize).not.toHaveBeenCalled() + expect(protocol.logoff).toHaveBeenCalled() + expect(protocol.login).toHaveBeenCalledWith(expect.objectContaining({ + authToken, + flush: true + })) + + const expectedError = newError('something wrong is not right.') + onLoginErrors.forEach(onError => onError(expectedError)) + await expect(connectionPromise).rejects.toBe(expectedError) + }) + }) }) describe('when protocol does not support re-auth', () => { diff --git a/packages/core/src/auth-token-manager.ts b/packages/core/src/auth-token-manager.ts index 4a633c104..59aa06051 100644 --- a/packages/core/src/auth-token-manager.ts +++ b/packages/core/src/auth-token-manager.ts @@ -24,7 +24,7 @@ import { util } from './internal' /** * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. * @interface - * @since 5.6 + * @since 5.7 */ export default class AuthTokenManager { /** @@ -44,7 +44,7 @@ export default class AuthTokenManager { /** * Interface which defines an {@link AuthToken} with an expiry data time associated * @interface - * @since 5.6 + * @since 5.7 */ export class TemporalAuthData { public readonly token: AuthToken From b74617ea49661cf25e82c561aff1a576e8aa7d30 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 27 Feb 2023 19:24:37 +0100 Subject: [PATCH 51/70] sync deno --- packages/neo4j-driver-deno/lib/core/auth-token-manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts index de0c52e40..76dbc137c 100644 --- a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -24,7 +24,7 @@ import { util } from './internal/index.ts' /** * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. * @interface - * @since 5.6 + * @since 5.7 */ export default class AuthTokenManager { /** @@ -44,7 +44,7 @@ export default class AuthTokenManager { /** * Interface which defines an {@link AuthToken} with an expiry data time associated * @interface - * @since 5.6 + * @since 5.7 */ export class TemporalAuthData { public readonly token: AuthToken From 90dd4eaf3f1ad8f6a54c20786131c7d34dacae6d Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 6 Mar 2023 12:42:53 +0100 Subject: [PATCH 52/70] Implement changes from testkit --- .../connection-provider-direct.js | 1 + .../connection-provider-routing.js | 1 + .../connection-provider-direct.test.js | 14 ++-- .../connection-provider-routing.test.js | 64 ++++++++----------- .../connection-provider-direct.js | 1 + .../connection-provider-routing.js | 1 + packages/testkit-backend/src/context.js | 37 +++++++---- .../src/request-handlers-rx.js | 7 +- .../testkit-backend/src/request-handlers.js | 54 ++++++++-------- packages/testkit-backend/src/responses.js | 16 +++-- 10 files changed, 105 insertions(+), 91 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index e30012131..70e447007 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -66,6 +66,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { if (stickyConnection) { return stickyConnection } + return connection } return new DelegateConnection(connection, databaseSpecificErrorHandler) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index a157a98b0..45c1b8146 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -206,6 +206,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider if (stickyConnection) { return stickyConnection } + return connection } return new DelegateConnection(connection, databaseSpecificErrorHandler) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 9636f194f..41326ef4b 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -507,7 +507,7 @@ describe('user-switching', () => { jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newDirectConnectionProvider(address, pool) - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: '', @@ -515,9 +515,8 @@ describe('user-switching', () => { auth: acquireAuth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) }) }) @@ -574,7 +573,7 @@ describe('user-switching', () => { jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newDirectConnectionProvider(address, pool) - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: '', @@ -582,9 +581,8 @@ describe('user-switching', () => { auth: acquireAuth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) }) }) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 66a2dd28c..b80ceeffe 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3487,7 +3487,7 @@ describe.each([ ) const auth = acquireAuth - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: '', @@ -3495,9 +3495,8 @@ describe.each([ auth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) it('should return connection when try switch user on acquire [expired rt]', async () => { @@ -3530,7 +3529,7 @@ describe.each([ const auth = acquireAuth - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', @@ -3538,12 +3537,11 @@ describe.each([ auth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) - it('should return delegated connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { + it('should return connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) @@ -3575,7 +3573,7 @@ describe.each([ const auth = acquireAuth - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', @@ -3583,12 +3581,11 @@ describe.each([ auth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) - it('should delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { + it('should not delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) @@ -3612,7 +3609,7 @@ describe.each([ const auth = acquireAuth - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', @@ -3620,9 +3617,8 @@ describe.each([ auth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) }) }) @@ -3895,7 +3891,7 @@ describe.each([ ) const auth = acquireAuth - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: '', @@ -3903,9 +3899,8 @@ describe.each([ auth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) it('should return connection when try switch user on acquire [expired rt]', async () => { @@ -3938,7 +3933,7 @@ describe.each([ const auth = acquireAuth - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', @@ -3946,12 +3941,11 @@ describe.each([ auth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) - it('should return delegated connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { + it('should return connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) @@ -3983,7 +3977,7 @@ describe.each([ const auth = acquireAuth - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', @@ -3991,12 +3985,11 @@ describe.each([ auth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) - it('should delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { + it('should not delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) @@ -4020,7 +4013,7 @@ describe.each([ const auth = acquireAuth - const delegatedConnection = await connectionProvider + const acquiredConnection = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', @@ -4028,9 +4021,8 @@ describe.each([ auth }) - expect(delegatedConnection).toBeInstanceOf(DelegateConnection) - expect(delegatedConnection._delegate).toBe(connection) - expect(connection._sticky).toEqual(false) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) }) }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 684ae0cea..443eb27dc 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -66,6 +66,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { if (stickyConnection) { return stickyConnection } + return connection } return new DelegateConnection(connection, databaseSpecificErrorHandler) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 83518e1b4..1ff26e16e 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -206,6 +206,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider if (stickyConnection) { return stickyConnection } + return connection } return new DelegateConnection(connection, databaseSpecificErrorHandler) diff --git a/packages/testkit-backend/src/context.js b/packages/testkit-backend/src/context.js index 56b65a280..439ae2b43 100644 --- a/packages/testkit-backend/src/context.js +++ b/packages/testkit-backend/src/context.js @@ -12,8 +12,9 @@ export default class Context { this._bookmarkSupplierRequests = {} this._notifyBookmarksRequests = {} this._bookmarksManagers = {} - this._authTokenProviders = {} - this._authTokenProviderRequests = {} + this._authTokenManagers = {} + this._authTokenManagerGetAuthRequests = {} + this._authTokenManagerOnAuthExpiredRequests = {} this._binder = binder this._environmentLogLevel = environmentLogLevel } @@ -163,32 +164,40 @@ export default class Context { delete this._bookmarksManagers[id] } - addAuthTokenProvider (authTokenProviderFactory) { + addAuthTokenManager (authTokenManagersFactory) { this._id++ - this._authTokenProviders[this._id] = authTokenProviderFactory(this._id) + this._authTokenManagers[this._id] = authTokenManagersFactory(this._id) return this._id } - getAuthTokenProvider (id) { - return this._authTokenProviders[id] + getAuthTokenManager (id) { + return this._authTokenManagers[id] } - removeAuthTokenProvider (id) { - delete this._authTokenProviders[id] + removeAuthTokenManager (id) { + delete this._authTokenManagers[id] } - addAuthTokenProviderRequest (resolve, reject) { - return this._add(this._authTokenProviderRequests, { + addAuthTokenManagerGetAuthRequest (resolve, reject) { + return this._add(this._authTokenManagerGetAuthRequests, { resolve, reject }) } - removeAuthTokenProviderRequest (id) { - delete this._authTokenProviderRequests[id] + getAuthTokenManagerGetAuthRequest (id) { + return this._authTokenManagerGetAuthRequests[id] } - getAuthTokenProviderRequest (id) { - return this._authTokenProviderRequests[id] + removeAuthTokenManagerGetAuthRequest (id) { + delete this._authTokenManagerGetAuthRequests[id] + } + + addAuthTokenManagerOnAuthExpiredRequest (request) { + return this._add(this._authTokenManagerOnAuthExpiredRequests, request) + } + + removeAuthTokenManagerOnAuthExpiredRequest (id) { + delete this._authTokenManagerOnAuthExpiredRequests[id] } _add (map, object) { diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index df64974a2..ef24c8462 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -25,9 +25,10 @@ export { BookmarksConsumerCompleted, StartSubTest, ExecuteQuery, - NewAuthTokenProvider, - AuthTokenProviderCompleted, - AuthTokenProviderClose, + NewAuthTokenManager, + AuthTokenManagerClose, + AuthTokenManagerGetAuthCompleted, + AuthTokenManagerOnAuthExpiredCompleted, FakeTimeInstall, FakeTimeTick, FakeTimeUninstall diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 5b60886c7..668a4836c 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -12,7 +12,7 @@ export function NewDriver ({ neo4j }, context, data, wire) { const { uri, authorizationToken, - authTokenProviderId, + authTokenManagerId, userAgent, resolverRegistered, backwardsCompatibleAuth @@ -20,15 +20,13 @@ export function NewDriver ({ neo4j }, context, data, wire) { let parsedAuthToken = null - if (authorizationToken != null && authTokenProviderId != null) { - throw new Error('Can not set authorizationToken and authTokenProviderId') + if (authorizationToken != null && authTokenManagerId != null) { + throw new Error('Can not set authorizationToken and authTokenManagerId') } else if (authorizationToken) { const { data: authToken } = authorizationToken parsedAuthToken = context.binder.parseAuthToken(authToken) } else { - parsedAuthToken = neo4j.temporalAuthDataManager({ - getAuthData: context.getAuthTokenProvider(authTokenProviderId) - }) + parsedAuthToken = context.getAuthTokenManager(authTokenManagerId) } const resolver = resolverRegistered @@ -528,32 +526,36 @@ export function BookmarksConsumerCompleted ( notifyBookmarksRequest.resolve() } -export function NewAuthTokenProvider (_, context, _data, wire) { - const id = context.addAuthTokenProvider(authTokenProviderId => { - return () => new Promise((resolve, reject) => { - const id = context.addAuthTokenProviderRequest(resolve, reject) - wire.writeResponse(responses.AuthTokenProviderRequest({ id, authTokenProviderId })) - }) +export function NewAuthTokenManager (_, context, _data, wire) { + const id = context.addAuthTokenManager((authTokenManagerId) => { + return { + getToken: () => new Promise((resolve, reject) => { + const id = context.addAuthTokenManagerGetAuthRequest(resolve, reject) + wire.writeResponse(responses.AuthTokenManagerGetAuthRequest({ id, authTokenManagerId })) + }), + onTokenExpired: (auth) => { + const id = context.addAuthTokenManagerOnAuthExpiredRequest() + wire.writeResponse(responses.AuthTokenManagerOnAuthExpiredRequest({ id, authTokenManagerId, auth })) + } + } }) - wire.writeResponse(responses.AuthTokenProvider({ id })) + wire.writeResponse(responses.AuthTokenManager({ id })) } -export function AuthTokenProviderCompleted (_, context, { requestId, auth }, _wire) { - const request = context.getAuthTokenProviderRequest(requestId) - const renewableToken = { - expiry: auth.data.expiresInMs != null - ? new Date(new Date().getTime() + auth.data.expiresInMs) - : undefined, - token: context.binder.parseAuthToken(auth.data.auth.data) - } - request.resolve(renewableToken) - context.removeAuthTokenProviderRequest(requestId) +export function AuthTokenManagerClose (_, context, { id }, wire) { + context.removeAuthTokenManager(id) + wire.writeResponse(responses.AuthTokenManager({ id })) +} + +export function AuthTokenManagerGetAuthCompleted (_, context, { requestId, auth }) { + const request = context.getAuthTokenManagerGetAuthRequest(requestId) + request.resolve(auth.data) + context.removeAuthTokenManagerGetAuthRequest(requestId) } -export function AuthTokenProviderClose (_, context, { id }, wire) { - context.removeAuthTokenProvider(id) - wire.writeResponse(responses.AuthTokenProvider({ id })) +export function AuthTokenManagerOnAuthExpiredCompleted (_, context, { requestId, auth }) { + context.removeAuthTokenManagerOnAuthExpiredRequest(requestId) } export function GetRoutingTable (_, context, { driverId, database }, wire) { diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index 5c21fe2cc..c7c2e7eef 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -103,12 +103,20 @@ export function EagerResult ({ keys, records, summary }, { binder }) { }) } -export function AuthTokenProvider ({ id }) { - return response('AuthTokenProvider', { id }) +export function AuthTokenManager ({ id }) { + return response('AuthTokenManager', { id }) } -export function AuthTokenProviderRequest ({ id, authTokenProviderId }) { - return response('AuthTokenProviderRequest', { id, authTokenProviderId }) +export function AuthTokenManagerGetAuthRequest ({ id, authTokenManagerId }) { + return response('AuthTokenManagerGetAuthRequest', { id, authTokenManagerId }) +} + +export function AuthorizationToken (data) { + return response('AuthorizationToken', data) +} + +export function AuthTokenManagerOnAuthExpiredRequest ({ id, authTokenManagerId, auth }) { + return response('AuthTokenManagerOnAuthExpiredRequest', { id, authTokenManagerId, auth: AuthorizationToken(auth) }) } export function DriverIsAuthenticated ({ id, authenticated }) { From efc22aa3bb1ff9a2bca3108c988cb4dd62e70c81 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 6 Mar 2023 15:34:37 +0100 Subject: [PATCH 53/70] Add temporal token tests --- packages/testkit-backend/src/context.js | 13 ++++++++ .../src/request-handlers-rx.js | 2 ++ .../testkit-backend/src/request-handlers.js | 30 +++++++++++++++++-- packages/testkit-backend/src/responses.js | 8 +++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/testkit-backend/src/context.js b/packages/testkit-backend/src/context.js index 439ae2b43..0754a921d 100644 --- a/packages/testkit-backend/src/context.js +++ b/packages/testkit-backend/src/context.js @@ -15,6 +15,7 @@ export default class Context { this._authTokenManagers = {} this._authTokenManagerGetAuthRequests = {} this._authTokenManagerOnAuthExpiredRequests = {} + this._temporalAuthTokenProviderRequests = {} this._binder = binder this._environmentLogLevel = environmentLogLevel } @@ -200,6 +201,18 @@ export default class Context { delete this._authTokenManagerOnAuthExpiredRequests[id] } + addTemporalAuthTokenProviderRequest (resolve, reject) { + return this._add(this._temporalAuthTokenProviderRequests, { resolve, reject }) + } + + getTemporalAuthTokenProviderRequest (id) { + return this._temporalAuthTokenProviderRequests[id] + } + + removeTemporalAuthTokenProviderRequest (id) { + delete this._temporalAuthTokenProviderRequests[id] + } + _add (map, object) { this._id++ map[this._id] = object diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index ef24c8462..bba152dd7 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -26,9 +26,11 @@ export { StartSubTest, ExecuteQuery, NewAuthTokenManager, + NewTemporalAuthTokenManager, AuthTokenManagerClose, AuthTokenManagerGetAuthCompleted, AuthTokenManagerOnAuthExpiredCompleted, + TemporalAuthTokenProviderCompleted, FakeTimeInstall, FakeTimeTick, FakeTimeUninstall diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 668a4836c..e0d55b438 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -452,7 +452,7 @@ export function ResolverResolutionCompleted ( } export function NewBookmarkManager ( - neo4j, + { neo4j }, context, { initialBookmarks, @@ -543,6 +543,19 @@ export function NewAuthTokenManager (_, context, _data, wire) { wire.writeResponse(responses.AuthTokenManager({ id })) } +export function NewTemporalAuthTokenManager ({ neo4j }, context, _, wire) { + const id = context.addAuthTokenManager((temporalAuthTokenManagerId) => { + return neo4j.temporalAuthDataManager({ + getAuthData: () => new Promise((resolve, reject) => { + const id = context.addTemporalAuthTokenProviderRequest(resolve, reject) + wire.writeResponse(responses.TemporalAuthTokenProviderRequest({ id, temporalAuthTokenManagerId })) + }) + }) + }) + + wire.writeResponse(responses.TemporalAuthTokenManager({ id })) +} + export function AuthTokenManagerClose (_, context, { id }, wire) { context.removeAuthTokenManager(id) wire.writeResponse(responses.AuthTokenManager({ id })) @@ -554,10 +567,21 @@ export function AuthTokenManagerGetAuthCompleted (_, context, { requestId, auth context.removeAuthTokenManagerGetAuthRequest(requestId) } -export function AuthTokenManagerOnAuthExpiredCompleted (_, context, { requestId, auth }) { +export function AuthTokenManagerOnAuthExpiredCompleted (_, context, { requestId }) { context.removeAuthTokenManagerOnAuthExpiredRequest(requestId) } +export function TemporalAuthTokenProviderCompleted (_, context, { requestId, auth }) { + const request = context.getTemporalAuthTokenProviderRequest(requestId) + request.resolve({ + expiry: auth.data.expiresInMs != null + ? new Date(new Date().getTime() + auth.data.expiresInMs) + : undefined, + token: context.binder.parseAuthToken(auth.data.auth.data) + }) + context.removeTemporalAuthTokenProviderRequest(requestId) +} + export function GetRoutingTable (_, context, { driverId, database }, wire) { const driver = context.getDriver(driverId) const routingTable = @@ -601,7 +625,7 @@ export function ForcedRoutingTableUpdate (_, context, { driverId, database, book } } -export function ExecuteQuery (neo4j, context, { driverId, cypher, params, config }, wire) { +export function ExecuteQuery ({ neo4j }, context, { driverId, cypher, params, config }, wire) { const driver = context.getDriver(driverId) if (params) { for (const [key, value] of Object.entries(params)) { diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index c7c2e7eef..101a5cc19 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -119,6 +119,14 @@ export function AuthTokenManagerOnAuthExpiredRequest ({ id, authTokenManagerId, return response('AuthTokenManagerOnAuthExpiredRequest', { id, authTokenManagerId, auth: AuthorizationToken(auth) }) } +export function TemporalAuthTokenManager ({ id }) { + return response('TemporalAuthTokenManager', { id }) +} + +export function TemporalAuthTokenProviderRequest ({ id, temporalAuthTokenManagerId }) { + return response('TemporalAuthTokenProviderRequest', { id, temporalAuthTokenManagerId }) +} + export function DriverIsAuthenticated ({ id, authenticated }) { return response('DriverIsAuthenticated', { id, authenticated }) } From 7a80acf12efa410877b7a4449ee422238906e0b3 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 8 Mar 2023 14:21:43 +0100 Subject: [PATCH 54/70] rename login => logon --- .../connection-provider-pooled.js | 2 +- .../src/connection/connection-channel.js | 4 ++-- .../connection/connection-channel.test.js | 22 +++++++++---------- .../connection-provider-pooled.js | 2 +- .../connection/connection-channel.js | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 23e3c7a9d..c3fdcea65 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -170,7 +170,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { const connection = await this._connectionPool.acquire({ auth, skipReAuth: true }, address) connectionsToRelease.push(connection) - const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogin() + const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogon() if (!connection.supportsReAuth && !allowStickyConnection) { throw newError('Driver is connected to a database that does not support user switch.') diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 5f743f987..760aa213c 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -203,7 +203,7 @@ export default class ChannelConnection extends Connection { onError: reject }) - this._protocol.login({ + this._protocol.logon({ authToken, onError: reject, onComplete: () => resolve(this), @@ -213,7 +213,7 @@ export default class ChannelConnection extends Connection { } this._protocol.logoff() - this._protocol.login({ authToken, flush: true }) + this._protocol.logon({ authToken, flush: true }) return this } diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index bdf61f04d..0b18ebd8a 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -173,7 +173,7 @@ describe('ChannelConnection', () => { const protocol = { initialize: jest.fn(observer => observer.onComplete({})), logoff: jest.fn(() => undefined), - login: jest.fn(() => undefined), + logon: jest.fn(() => undefined), initialized: true, supportsLogoff: true } @@ -185,7 +185,7 @@ describe('ChannelConnection', () => { expect(protocol.initialize).not.toHaveBeenCalled() expect(protocol.logoff).toHaveBeenCalledWith() - expect(protocol.login).toHaveBeenCalledWith({ authToken, flush: true }) + expect(protocol.logon).toHaveBeenCalledWith({ authToken, flush: true }) expect(connection.authToken).toEqual(authToken) }) @@ -199,7 +199,7 @@ describe('ChannelConnection', () => { const protocol = { initialize: jest.fn(observer => observer.onComplete({})), logoff: jest.fn(() => undefined), - login: jest.fn(({ onComplete }) => onCompleteObservers.push(onComplete)), + logon: jest.fn(({ onComplete }) => onCompleteObservers.push(onComplete)), initialized: true, supportsLogoff: true } @@ -215,7 +215,7 @@ describe('ChannelConnection', () => { expect(protocol.initialize).not.toHaveBeenCalled() expect(protocol.logoff).toHaveBeenCalled() - expect(protocol.login).toHaveBeenCalledWith(expect.objectContaining({ + expect(protocol.logon).toHaveBeenCalledWith(expect.objectContaining({ authToken, flush: true })) @@ -235,7 +235,7 @@ describe('ChannelConnection', () => { const protocol = { initialize: jest.fn(observer => observer.onComplete({})), logoff: jest.fn(({ onError }) => onLogoffErrors.push(onError)), - login: jest.fn(() => undefined), + logon: jest.fn(() => undefined), initialized: true, supportsLogoff: true } @@ -251,7 +251,7 @@ describe('ChannelConnection', () => { expect(protocol.initialize).not.toHaveBeenCalled() expect(protocol.logoff).toHaveBeenCalled() - expect(protocol.login).toHaveBeenCalledWith(expect.objectContaining({ + expect(protocol.logon).toHaveBeenCalledWith(expect.objectContaining({ authToken, flush: true })) @@ -261,7 +261,7 @@ describe('ChannelConnection', () => { await expect(connectionPromise).rejects.toBe(expectedError) }) - it('should notify login errors', async () => { + it('should notify logon errors', async () => { const authToken = { scheme: 'none' } @@ -270,7 +270,7 @@ describe('ChannelConnection', () => { const protocol = { initialize: jest.fn(observer => observer.onComplete({})), logoff: jest.fn(() => undefined), - login: jest.fn(({ onError }) => onLoginErrors.push(onError)), + logon: jest.fn(({ onError }) => onLoginErrors.push(onError)), initialized: true, supportsLogoff: true } @@ -286,7 +286,7 @@ describe('ChannelConnection', () => { expect(protocol.initialize).not.toHaveBeenCalled() expect(protocol.logoff).toHaveBeenCalled() - expect(protocol.login).toHaveBeenCalledWith(expect.objectContaining({ + expect(protocol.logon).toHaveBeenCalledWith(expect.objectContaining({ authToken, flush: true })) @@ -306,7 +306,7 @@ describe('ChannelConnection', () => { const protocol = { initialize: jest.fn(observer => observer.onComplete({})), logoff: jest.fn(() => undefined), - login: jest.fn(() => undefined), + logon: jest.fn(() => undefined), initialized: true, supportsLogoff: false } @@ -320,7 +320,7 @@ describe('ChannelConnection', () => { expect(protocol.initialize).not.toHaveBeenCalled() expect(protocol.logoff).not.toHaveBeenCalled() - expect(protocol.login).not.toHaveBeenCalled() + expect(protocol.logon).not.toHaveBeenCalled() expect(connection.authToken).toEqual(null) }) }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 7f930e6e1..fecc27950 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -170,7 +170,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { const connection = await this._connectionPool.acquire({ auth, skipReAuth: true }, address) connectionsToRelease.push(connection) - const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogin() + const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogon() if (!connection.supportsReAuth && !allowStickyConnection) { throw newError('Driver is connected to a database that does not support user switch.') diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index b090f510a..ea15312cb 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -203,7 +203,7 @@ export default class ChannelConnection extends Connection { onError: reject }) - this._protocol.login({ + this._protocol.logon({ authToken, onError: reject, onComplete: () => resolve(this), @@ -213,7 +213,7 @@ export default class ChannelConnection extends Connection { } this._protocol.logoff() - this._protocol.login({ authToken, flush: true }) + this._protocol.logon({ authToken, flush: true }) return this } From 1eb29b660f4631f7626b07659f6acc04cf45d09d Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 8 Mar 2023 14:53:11 +0100 Subject: [PATCH 55/70] rename supportsLogoff => supportsReAuth --- .../src/connection/connection-channel.js | 4 ++-- .../test/connection/connection-channel.test.js | 10 +++++----- .../bolt-connection/connection/connection-channel.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/bolt-connection/src/connection/connection-channel.js b/packages/bolt-connection/src/connection/connection-channel.js index 760aa213c..38bcd2fa5 100644 --- a/packages/bolt-connection/src/connection/connection-channel.js +++ b/packages/bolt-connection/src/connection/connection-channel.js @@ -165,7 +165,7 @@ export default class ChannelConnection extends Connection { } get supportsReAuth () { - return this._protocol.supportsLogoff + return this._protocol.supportsReAuth } get id () { @@ -187,7 +187,7 @@ export default class ChannelConnection extends Connection { * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ async connect (userAgent, authToken, waitReAuth) { - if (this._protocol.initialized && !this._protocol.supportsLogoff) { + if (this._protocol.initialized && !this._protocol.supportsReAuth) { throw newError('Connection does not support re-auth') } diff --git a/packages/bolt-connection/test/connection/connection-channel.test.js b/packages/bolt-connection/test/connection/connection-channel.test.js index 0b18ebd8a..446747647 100644 --- a/packages/bolt-connection/test/connection/connection-channel.test.js +++ b/packages/bolt-connection/test/connection/connection-channel.test.js @@ -175,7 +175,7 @@ describe('ChannelConnection', () => { logoff: jest.fn(() => undefined), logon: jest.fn(() => undefined), initialized: true, - supportsLogoff: true + supportsReAuth: true } const protocolSupplier = () => protocol @@ -201,7 +201,7 @@ describe('ChannelConnection', () => { logoff: jest.fn(() => undefined), logon: jest.fn(({ onComplete }) => onCompleteObservers.push(onComplete)), initialized: true, - supportsLogoff: true + supportsReAuth: true } const protocolSupplier = () => protocol @@ -237,7 +237,7 @@ describe('ChannelConnection', () => { logoff: jest.fn(({ onError }) => onLogoffErrors.push(onError)), logon: jest.fn(() => undefined), initialized: true, - supportsLogoff: true + supportsReAuth: true } const protocolSupplier = () => protocol @@ -272,7 +272,7 @@ describe('ChannelConnection', () => { logoff: jest.fn(() => undefined), logon: jest.fn(({ onError }) => onLoginErrors.push(onError)), initialized: true, - supportsLogoff: true + supportsReAuth: true } const protocolSupplier = () => protocol @@ -308,7 +308,7 @@ describe('ChannelConnection', () => { logoff: jest.fn(() => undefined), logon: jest.fn(() => undefined), initialized: true, - supportsLogoff: false + supportsReAuth: false } const protocolSupplier = () => protocol diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js index ea15312cb..a974d17c7 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection/connection-channel.js @@ -165,7 +165,7 @@ export default class ChannelConnection extends Connection { } get supportsReAuth () { - return this._protocol.supportsLogoff + return this._protocol.supportsReAuth } get id () { @@ -187,7 +187,7 @@ export default class ChannelConnection extends Connection { * @return {Promise} promise resolved with the current connection if connection is successful. Rejected promise otherwise. */ async connect (userAgent, authToken, waitReAuth) { - if (this._protocol.initialized && !this._protocol.supportsLogoff) { + if (this._protocol.initialized && !this._protocol.supportsReAuth) { throw newError('Connection does not support re-auth') } From 95ee03562b0bf10453371c6504b2a26fa4220e1c Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 21 Mar 2023 13:02:50 +0100 Subject: [PATCH 56/70] Enable AuthTokenManager tests for testkit --- packages/testkit-backend/src/feature/common.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/testkit-backend/src/feature/common.js b/packages/testkit-backend/src/feature/common.js index 2170ed3ce..0a2235f98 100644 --- a/packages/testkit-backend/src/feature/common.js +++ b/packages/testkit-backend/src/feature/common.js @@ -4,6 +4,7 @@ const features = [ 'Feature:Auth:Custom', 'Feature:Auth:Kerberos', 'Feature:Auth:Bearer', + 'Feature:Auth:Managed', 'Feature:API:BookmarkManager', 'Feature:API:Session:AuthConfig', 'Feature:API:SSLConfig', From 8b20615f72f418fb751a3e8074d160db7fccdc47 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 21 Mar 2023 13:08:09 +0100 Subject: [PATCH 57/70] Marking re-auth and user-switch as experimental/preview --- packages/core/src/auth-token-manager.ts | 2 ++ packages/core/src/driver.ts | 1 + packages/neo4j-driver-deno/lib/core/auth-token-manager.ts | 2 ++ packages/neo4j-driver-deno/lib/core/driver.ts | 1 + 4 files changed, 6 insertions(+) diff --git a/packages/core/src/auth-token-manager.ts b/packages/core/src/auth-token-manager.ts index 59aa06051..8ae3de212 100644 --- a/packages/core/src/auth-token-manager.ts +++ b/packages/core/src/auth-token-manager.ts @@ -44,6 +44,7 @@ export default class AuthTokenManager { /** * Interface which defines an {@link AuthToken} with an expiry data time associated * @interface + * @experimental Exposed as preview feature. * @since 5.7 */ export class TemporalAuthData { @@ -80,6 +81,7 @@ export class TemporalAuthData { * @param {object} param0 - The params * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token * @returns {AuthTokenManager} The temporal auth data manager. + * @experimental Exposed as preview feature. */ export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => Promise }): AuthTokenManager { if (typeof getAuthData !== 'function') { diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 3e1b19740..40c35066c 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -209,6 +209,7 @@ class SessionConfig { * configured with an auth. * * @type {AuthToken|undefined} + * @experimental Exposed as preview feature. * @see {@link driver} */ this.auth = undefined diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts index 76dbc137c..fb9a38116 100644 --- a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -44,6 +44,7 @@ export default class AuthTokenManager { /** * Interface which defines an {@link AuthToken} with an expiry data time associated * @interface + * @experimental Exposed as preview feature. * @since 5.7 */ export class TemporalAuthData { @@ -80,6 +81,7 @@ export class TemporalAuthData { * @param {object} param0 - The params * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token * @returns {AuthTokenManager} The temporal auth data manager. + * @experimental Exposed as preview feature. */ export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => Promise }): AuthTokenManager { if (typeof getAuthData !== 'function') { diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 2f854c007..8c5d5cd92 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -209,6 +209,7 @@ class SessionConfig { * configured with an auth. * * @type {AuthToken|undefined} + * @experimental Exposed as preview feature. * @see {@link driver} */ this.auth = undefined From fc6bdf6f29f1979417ad585153b6da256e4f3bc0 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 21 Mar 2023 15:38:47 +0100 Subject: [PATCH 58/70] Retry on token expired when token is static token --- .../connection-provider-direct.js | 10 +-- .../connection-provider-pooled.js | 21 ++++- .../connection-provider-routing.js | 12 +-- .../connection-provider-direct.test.js | 47 ++++++++++- .../connection-provider-routing.test.js | 82 ++++++++++++++++++- packages/core/src/auth-token-manager.ts | 40 +++++++++ packages/core/src/index.ts | 4 +- .../connection-provider-direct.js | 10 +-- .../connection-provider-pooled.js | 21 ++++- .../connection-provider-routing.js | 12 +-- .../lib/core/auth-token-manager.ts | 40 +++++++++ packages/neo4j-driver-deno/lib/core/index.ts | 4 +- packages/neo4j-driver-deno/lib/mod.ts | 17 ++-- packages/neo4j-driver-lite/src/index.ts | 17 ++-- packages/neo4j-driver/src/index.js | 17 ++-- 15 files changed, 273 insertions(+), 81 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 70e447007..3873d8af4 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -77,15 +77,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { `Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - this._authenticationProvider.handleError({ connection, code: error.code }) - - if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { - this._connectionPool.apply(address, (conn) => { conn.authToken = null }) - } - - connection.close().catch(() => undefined) - - return error + return super._handleAuthorizationExpired(error, address, connection) } async _hasProtocolVersion (versionPredicate) { diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index c3fdcea65..538d1083e 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -19,7 +19,7 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection' import Pool, { PoolConfig } from '../pool' -import { error, ConnectionProvider, ServerInfo, newError } from 'neo4j-driver-core' +import { error, ConnectionProvider, ServerInfo, newError, isStaticAuthTokenManger } from 'neo4j-driver-core' import AuthenticationProvider from './authentication-provider' import { object } from '../lang' @@ -41,6 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log + this._authTokenManager = authTokenManager this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent }) this._createChannelConnection = createChannelConnectionHook || @@ -227,4 +228,22 @@ export default class PooledConnectionProvider extends ConnectionProvider { static _removeIdleObserverOnConnection (conn) { conn._updateCurrentObserver() } + + _handleAuthorizationExpired (error, address, connection) { + this._authenticationProvider.handleError({ connection, code: error.code }) + + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { + this._connectionPool.apply(address, (conn) => { conn.authToken = null }) + } + + if (connection) { + connection.close().catch(() => undefined) + } + + if (error.code === 'Neo.ClientError.Security.TokenExpired' && !isStaticAuthTokenManger(this._authTokenManager)) { + error.retriable = true + } + + return error + } } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 45c1b8146..1bc82587c 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -117,17 +117,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - this._authenticationProvider.handleError({ connection, code: error.code }) - - if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { - this._connectionPool.apply(address, (conn) => { conn.authToken = null }) - } - - if (connection) { - connection.close().catch(() => undefined) - } - - return error + return super._handleAuthorizationExpired(error, address, connection, database) } _handleWriteFailure (error, address, database) { diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 41326ef4b..79a0f04d7 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -20,7 +20,7 @@ import DirectConnectionProvider from '../../src/connection-provider/connection-provider-direct' import { Pool } from '../../src/pool' import { Connection, DelegateConnection } from '../../src/connection' -import { internal, newError, ServerInfo } from 'neo4j-driver-core' +import { internal, newError, ServerInfo, staticAuthTokenManager, temporalAuthDataManager } from 'neo4j-driver-core' import AuthenticationProvider from '../../src/connection-provider/authentication-provider' import { functional } from '../../src/lang' @@ -206,6 +206,46 @@ it('should call authenticationAuthProvider.handleError when TokenExpired happens expect(handleError).toBeCalledWith({ connection: conn, code: 'Neo.ClientError.Security.TokenExpired' }) }) +it('should change error to retriable when error when TokenExpired happens and staticAuthTokenManager is not being used', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connectionProvider = newDirectConnectionProvider(address, pool, temporalAuthDataManager({ getAuthData: () => null })) + + const conn = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: '' + }) + + const expectedError = newError( + 'Message', + 'Neo.ClientError.Security.TokenExpired' + ) + + const error = conn.handleAndTransformError(expectedError, address) + + expect(error.retriable).toBe(true) +}) + +it('should not change error to retriable when error when TokenExpired happens and staticAuthTokenManager is being used', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connectionProvider = newDirectConnectionProvider(address, pool, staticAuthTokenManager({ authToken: null })) + + const conn = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: '' + }) + + const expectedError = newError( + 'Message', + 'Neo.ClientError.Security.TokenExpired' + ) + + const error = conn.handleAndTransformError(expectedError, address) + + expect(error.retriable).toBe(false) +}) + describe('constructor', () => { describe('newPool', () => { const server0 = ServerAddress.fromUrl('localhost:123') @@ -762,12 +802,13 @@ describe('.verifyConnectivityAndGetServerInfo()', () => { }) }) -function newDirectConnectionProvider (address, pool) { +function newDirectConnectionProvider (address, pool, authTokenManager) { const connectionProvider = new DirectConnectionProvider({ id: 0, config: {}, log: Logger.noOp(), - address: address + address: address, + authTokenManager }) connectionProvider._connectionPool = pool return connectionProvider diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index b80ceeffe..92a85d3e1 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -24,7 +24,9 @@ import { Integer, int, internal, - ServerInfo + ServerInfo, + staticAuthTokenManager, + temporalAuthDataManager } from 'neo4j-driver-core' import { RoutingTable } from '../../src/rediscovery/' import { Pool } from '../../src/pool' @@ -1703,6 +1705,84 @@ describe.each([ expect(error).toBe(expectedError) }) + it.each(usersDataSet)('should change error to retriable when error when TokenExpired happens and staticAuthTokenManager is not being used [user=%s]', async (user) => { + const pool = newPool() + const connectionProvider = newRoutingConnectionProvider( + [ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + connectionProvider._authTokenManager = temporalAuthDataManager({ getAuthData: () => null }) + + const error = newError( + 'Message', + 'Neo.ClientError.Security.TokenExpired' + ) + + const server2Connection = await connectionProvider.acquireConnection({ + accessMode: 'WRITE', + database: null, + impersonatedUser: user + }) + + const server3Connection = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: null, + impersonatedUser: user + }) + + const error1 = server3Connection.handleAndTransformError(error, server3) + const error2 = server2Connection.handleAndTransformError(error, server2) + + expect(error1.retriable).toBe(true) + expect(error2.retriable).toBe(true) + }) + + it.each(usersDataSet)('should not change error to retriable when error when TokenExpired happens and staticAuthTokenManager is being used [user=%s]', async (user) => { + const pool = newPool() + const connectionProvider = newRoutingConnectionProvider( + [ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + connectionProvider._authTokenManager = staticAuthTokenManager({ authToken: null }) + + const error = newError( + 'Message', + 'Neo.ClientError.Security.TokenExpired' + ) + + const server2Connection = await connectionProvider.acquireConnection({ + accessMode: 'WRITE', + database: null, + impersonatedUser: user + }) + + const server3Connection = await connectionProvider.acquireConnection({ + accessMode: 'READ', + database: null, + impersonatedUser: user + }) + + const error1 = server3Connection.handleAndTransformError(error, server3) + const error2 = server2Connection.handleAndTransformError(error, server2) + + expect(error1.retriable).toBe(false) + expect(error2.retriable).toBe(false) + }) + it.each(usersDataSet)('should use resolved seed router after accepting table with no writers [user=%s]', (user, done) => { const routingTable1 = newRoutingTable( null, diff --git a/packages/core/src/auth-token-manager.ts b/packages/core/src/auth-token-manager.ts index 8ae3de212..4b47772d1 100644 --- a/packages/core/src/auth-token-manager.ts +++ b/packages/core/src/auth-token-manager.ts @@ -90,6 +90,30 @@ export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => P return new TemporalAuthDataManager(getAuthData) } +/** + * Create a {@link AuthTokenManager} for handle static {@link AuthToken} + * + * @private + * @param {param} args - The args + * @param {AuthToken} args.authToken - The static auth token which will always used in the driver. + * @returns {AuthTokenManager} The temporal auth data manager. + */ +export function staticAuthTokenManager ({ authToken }: { authToken: AuthToken }): AuthTokenManager { + return new StaticAuthTokenManager(authToken) +} + +/** + * Checks if the manager is a StaticAuthTokenManager + * + * @private + * @experimental + * @param {AuthTokenManager} manager The auth token manager to be checked. + * @returns {boolean} Manager is StaticAuthTokenManager + */ +export function isStaticAuthTokenManger (manager: AuthTokenManager): manager is StaticAuthTokenManager { + return manager instanceof StaticAuthTokenManager +} + interface TokenRefreshObserver { onCompleted: (data: TemporalAuthData) => void onError: (error: Error) => void @@ -171,3 +195,19 @@ class TemporalAuthDataManager implements AuthTokenManager { }) } } + +class StaticAuthTokenManager implements AuthTokenManager { + constructor ( + private readonly _authToken: AuthToken + ) { + + } + + getToken (): AuthToken { + return this._authToken + } + + onTokenExpired (_: AuthToken): void { + // nothing to do here + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 80f2eb209..341b74039 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -87,7 +87,7 @@ import Session, { TransactionConfig } from './session' import Driver, * as driver from './driver' import auth from './auth' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager' -import AuthTokenManager, { temporalAuthDataManager, TemporalAuthData } from './auth-token-manager' +import AuthTokenManager, { temporalAuthDataManager, staticAuthTokenManager, isStaticAuthTokenManger, TemporalAuthData } from './auth-token-manager' import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver' import * as types from './types' import * as json from './json' @@ -233,6 +233,8 @@ export { auth, bookmarkManager, temporalAuthDataManager, + staticAuthTokenManager, + isStaticAuthTokenManger, routing, resultTransformers, notificationCategory, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index 443eb27dc..facd5c4bc 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -77,15 +77,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { `Direct driver ${this._id} will close connection to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - this._authenticationProvider.handleError({ connection, code: error.code }) - - if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { - this._connectionPool.apply(address, (conn) => { conn.authToken = null }) - } - - connection.close().catch(() => undefined) - - return error + return super._handleAuthorizationExpired(error, address, connection) } async _hasProtocolVersion (versionPredicate) { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index fecc27950..8de11a907 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -19,7 +19,7 @@ import { createChannelConnection, ConnectionErrorHandler } from '../connection/index.js' import Pool, { PoolConfig } from '../pool/index.js' -import { error, ConnectionProvider, ServerInfo, newError } from '../../core/index.ts' +import { error, ConnectionProvider, ServerInfo, newError, isStaticAuthTokenManger } from '../../core/index.ts' import AuthenticationProvider from './authentication-provider.js' import { object } from '../lang/index.js' @@ -41,6 +41,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { this._id = id this._config = config this._log = log + this._authTokenManager = authTokenManager this._authenticationProvider = new AuthenticationProvider({ authTokenManager, userAgent }) this._createChannelConnection = createChannelConnectionHook || @@ -227,4 +228,22 @@ export default class PooledConnectionProvider extends ConnectionProvider { static _removeIdleObserverOnConnection (conn) { conn._updateCurrentObserver() } + + _handleAuthorizationExpired (error, address, connection) { + this._authenticationProvider.handleError({ connection, code: error.code }) + + if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { + this._connectionPool.apply(address, (conn) => { conn.authToken = null }) + } + + if (connection) { + connection.close().catch(() => undefined) + } + + if (error.code === 'Neo.ClientError.Security.TokenExpired' && !isStaticAuthTokenManger(this._authTokenManager)) { + error.retriable = true + } + + return error + } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index 1ff26e16e..d79bd9de5 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -117,17 +117,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider `Routing driver ${this._id} will close connections to ${address} for database '${database}' because of an error ${error.code} '${error.message}'` ) - this._authenticationProvider.handleError({ connection, code: error.code }) - - if (error.code === 'Neo.ClientError.Security.AuthorizationExpired') { - this._connectionPool.apply(address, (conn) => { conn.authToken = null }) - } - - if (connection) { - connection.close().catch(() => undefined) - } - - return error + return super._handleAuthorizationExpired(error, address, connection, database) } _handleWriteFailure (error, address, database) { diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts index fb9a38116..a71f9b945 100644 --- a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -90,6 +90,30 @@ export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => P return new TemporalAuthDataManager(getAuthData) } +/** + * Create a {@link AuthTokenManager} for handle static {@link AuthToken} + * + * @private + * @param {param} args - The args + * @param {AuthToken} args.authToken - The static auth token which will always used in the driver. + * @returns {AuthTokenManager} The temporal auth data manager. + */ +export function staticAuthTokenManager ({ authToken }: { authToken: AuthToken }): AuthTokenManager { + return new StaticAuthTokenManager(authToken) +} + +/** + * Checks if the manager is a StaticAuthTokenManager + * + * @private + * @experimental + * @param {AuthTokenManager} manager The auth token manager to be checked. + * @returns {boolean} Manager is StaticAuthTokenManager + */ +export function isStaticAuthTokenManger (manager: AuthTokenManager): manager is StaticAuthTokenManager { + return manager instanceof StaticAuthTokenManager +} + interface TokenRefreshObserver { onCompleted: (data: TemporalAuthData) => void onError: (error: Error) => void @@ -171,3 +195,19 @@ class TemporalAuthDataManager implements AuthTokenManager { }) } } + +class StaticAuthTokenManager implements AuthTokenManager { + constructor ( + private readonly _authToken: AuthToken + ) { + + } + + getToken (): AuthToken { + return this._authToken + } + + onTokenExpired (_: AuthToken): void { + // nothing to do here + } +} diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 2412d7add..1531b36c3 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -87,7 +87,7 @@ import Session, { TransactionConfig } from './session.ts' import Driver, * as driver from './driver.ts' import auth from './auth.ts' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager.ts' -import AuthTokenManager, { temporalAuthDataManager, TemporalAuthData } from './auth-token-manager.ts' +import AuthTokenManager, { temporalAuthDataManager, staticAuthTokenManager, isStaticAuthTokenManger, TemporalAuthData } from './auth-token-manager.ts' import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver.ts' import * as types from './types.ts' import * as json from './json.ts' @@ -233,6 +233,8 @@ export { auth, bookmarkManager, temporalAuthDataManager, + staticAuthTokenManager, + isStaticAuthTokenManger, routing, resultTransformers, notificationCategory, diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index b4ed0b258..3433a25cb 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -96,7 +96,8 @@ import { notificationFilterMinimumSeverityLevel, AuthTokenManager, temporalAuthDataManager, - TemporalAuthData + TemporalAuthData, + staticAuthTokenManager } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts import { @@ -139,17 +140,11 @@ function createAuthManager (authTokenOrProvider: AuthToken | AuthTokenManager): return authTokenOrProvider } - let token: AuthToken = authTokenOrProvider + let authToken: AuthToken = authTokenOrProvider // Sanitize authority token. Nicer error from server when a scheme is set. - token = token ?? {} - token.scheme = token.scheme ?? 'none' - return temporalAuthDataManager({ - getAuthData: async function (): Promise { - return { - token - } - } - }) + authToken = authToken ?? {} + authToken.scheme = authToken.scheme ?? 'none' + return staticAuthTokenManager({ authToken }) } /** diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index cc297cabb..68a744a11 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -96,7 +96,8 @@ import { notificationFilterMinimumSeverityLevel, AuthTokenManager, temporalAuthDataManager, - TemporalAuthData + TemporalAuthData, + staticAuthTokenManager } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -138,17 +139,11 @@ function createAuthManager (authTokenOrProvider: AuthToken | AuthTokenManager): return authTokenOrProvider } - let token: AuthToken = authTokenOrProvider + let authToken: AuthToken = authTokenOrProvider // Sanitize authority token. Nicer error from server when a scheme is set. - token = token ?? {} - token.scheme = token.scheme ?? 'none' - return temporalAuthDataManager({ - getAuthData: async function (): Promise { - return { - token - } - } - }) + authToken = authToken ?? {} + authToken.scheme = authToken.scheme ?? 'none' + return staticAuthTokenManager({ authToken }) } /** diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index 34a7e02bb..f4dc7b7c3 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -74,7 +74,8 @@ import { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager + temporalAuthDataManager, + staticAuthTokenManager } from 'neo4j-driver-core' import { DirectConnectionProvider, @@ -106,17 +107,11 @@ function createAuthManager (authTokenOrManager) { return authTokenOrManager } - let token = authTokenOrManager + let authToken = authTokenOrManager // Sanitize authority token. Nicer error from server when a scheme is set. - token = token || {} - token.scheme = token.scheme || 'none' - return temporalAuthDataManager({ - getAuthData: async function () { - return { - token - } - } - }) + authToken = authToken || {} + authToken.scheme = authToken.scheme || 'none' + return staticAuthTokenManager({ authToken }) } /** From bfb40c382fad44ffa73a7f6d489c9b0a2b41d3e1 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 21 Mar 2023 15:58:21 +0100 Subject: [PATCH 59/70] Adjust documentation --- packages/core/src/auth-token-manager.ts | 18 +++++++++++++++-- packages/core/src/driver.ts | 5 +---- .../lib/core/auth-token-manager.ts | 20 ++++++++++++++++--- packages/neo4j-driver-deno/lib/core/driver.ts | 5 +---- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/core/src/auth-token-manager.ts b/packages/core/src/auth-token-manager.ts index 4b47772d1..7fa2863b6 100644 --- a/packages/core/src/auth-token-manager.ts +++ b/packages/core/src/auth-token-manager.ts @@ -24,11 +24,15 @@ import { util } from './internal' /** * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. * @interface + * @experimental Exposed as preview feature. * @since 5.7 */ export default class AuthTokenManager { /** - * Returns a valid token + * Returns a valid token. + * + * **Warning**: This method must only ever return auth information belonging to the same identity. + * Switching identities using the `AuthTokenManager` is undefined behavior. * * @returns {Promise|AuthToken} The valid auth token or a promise for a valid auth token */ @@ -36,6 +40,12 @@ export default class AuthTokenManager { throw new Error('Not Implemented') } + /** + * Called to notify a token expiration. + * + * @param {AuthToken} token The expired token. + * @return {void} + */ onTokenExpired (token: AuthToken): void { throw new Error('Not implemented') } @@ -78,8 +88,12 @@ export class TemporalAuthData { /** * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. * + * **Warning**: `getAuthData` must only ever return auth information belonging to the same identity. + * Switching identities using the `AuthTokenManager` is undefined behavior. + * * @param {object} param0 - The params - * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token + * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token. + * Must only ever return auth information belonging to the same identity. * @returns {AuthTokenManager} The temporal auth data manager. * @experimental Exposed as preview feature. */ diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 40c35066c..1d7e47d40 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -203,10 +203,7 @@ class SessionConfig { * session lifetime. * * **Warning**: This option is only enable by default when the driver is connected with Neo4j Database servers - * which supports Bolt 5.1 and onwards. For enabling backwards compatibility mode, please configure the - * driver with `backwardsCompatibleAuth` enable. Beware, the backwards compatible mode comes with - * a huge performance penalty since it uses a new connection for each unit of work run in a session - * configured with an auth. + * which supports Bolt 5.1 and onwards. * * @type {AuthToken|undefined} * @experimental Exposed as preview feature. diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts index a71f9b945..6d88f65b3 100644 --- a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -24,18 +24,28 @@ import { util } from './internal/index.ts' /** * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. * @interface + * @experimental Exposed as preview feature. * @since 5.7 */ export default class AuthTokenManager { /** - * Returns a valid token - * + * Returns a valid token. + * + * **Warning**: This method must only ever return auth information belonging to the same identity. + * Switching identities using the `AuthTokenManager` is undefined behavior. + * * @returns {Promise|AuthToken} The valid auth token or a promise for a valid auth token */ getToken (): Promise | AuthToken { throw new Error('Not Implemented') } + /** + * Called to notify a token expiration. + * + * @param {AuthToken} token The expired token. + * @return {void} + */ onTokenExpired (token: AuthToken): void { throw new Error('Not implemented') } @@ -77,9 +87,13 @@ export class TemporalAuthData { /** * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. + * + * **Warning**: `getAuthData` must only ever return auth information belonging to the same identity. + * Switching identities using the `AuthTokenManager` is undefined behavior. * * @param {object} param0 - The params - * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token + * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token. + * Must only ever return auth information belonging to the same identity. * @returns {AuthTokenManager} The temporal auth data manager. * @experimental Exposed as preview feature. */ diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 8c5d5cd92..23881fced 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -203,10 +203,7 @@ class SessionConfig { * session lifetime. * * **Warning**: This option is only enable by default when the driver is connected with Neo4j Database servers - * which supports Bolt 5.1 and onwards. For enabling backwards compatibility mode, please configure the - * driver with `backwardsCompatibleAuth` enable. Beware, the backwards compatible mode comes with - * a huge performance penalty since it uses a new connection for each unit of work run in a session - * configured with an auth. + * which supports Bolt 5.1 and onwards. * * @type {AuthToken|undefined} * @experimental Exposed as preview feature. From 302d43d53d759a21f041cd09a717fb0541b0ab00 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 22 Mar 2023 11:22:43 +0100 Subject: [PATCH 60/70] Remove backwardsCompatibilityAuth code --- .../connection-provider-direct.js | 13 +- .../connection-provider-pooled.js | 15 +- .../connection-provider-routing.js | 73 +- .../connection-provider-direct.test.js | 74 +- .../connection-provider-routing.test.js | 744 ++++-------------- packages/core/src/connection-provider.ts | 4 +- .../core/src/internal/connection-holder.ts | 10 +- packages/core/src/session.ts | 1 + packages/core/test/driver.test.ts | 4 +- .../connection-provider-direct.js | 13 +- .../connection-provider-pooled.js | 15 +- .../connection-provider-routing.js | 73 +- .../lib/core/auth-token-manager.ts | 14 +- .../lib/core/connection-provider.ts | 4 +- .../lib/core/internal/connection-holder.ts | 10 +- .../neo4j-driver-deno/lib/core/session.ts | 1 + .../testkit-backend/src/request-handlers.js | 4 +- 17 files changed, 247 insertions(+), 825 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js index 3873d8af4..fd5ed21bd 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-direct.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-direct.js @@ -47,7 +47,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, auth, allowStickyConnection, forceReAuth } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, auth, forceReAuth } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, handleAuthorizationExpired: (error, address, conn) => @@ -57,15 +57,11 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { const connection = await this._connectionPool.acquire({ auth, forceReAuth }, this._address) if (auth) { - const stickyConnection = await this._getStickyConnection({ + await this._verifyStickyConnection({ auth, connection, - address: this._address, - allowStickyConnection + address: this._address }) - if (stickyConnection) { - return stickyConnection - } return connection } @@ -132,9 +128,8 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } - async verifyAuthentication ({ auth, allowStickyConnection }) { + async verifyAuthentication ({ auth }) { return this._verifyAuthentication({ - allowStickyConnection, auth, getAddress: () => this._address }) diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js index 538d1083e..2c297daa9 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-pooled.js @@ -164,7 +164,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { return serverInfo } - async _verifyAuthentication ({ getAddress, auth, allowStickyConnection }) { + async _verifyAuthentication ({ getAddress, auth }) { const connectionsToRelease = [] try { const address = await getAddress() @@ -173,7 +173,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogon() - if (!connection.supportsReAuth && !allowStickyConnection) { + if (!connection.supportsReAuth) { throw newError('Driver is connected to a database that does not support user switch.') } if (lastMessageIsNotLogin && connection.supportsReAuth) { @@ -194,21 +194,14 @@ export default class PooledConnectionProvider extends ConnectionProvider { } } - async _getStickyConnection ({ auth, connection, address, allowStickyConnection }) { + async _verifyStickyConnection ({ auth, connection, address }) { const connectionWithSameCredentials = object.equals(auth, connection.authToken) const shouldCreateStickyConnection = !connectionWithSameCredentials connection._sticky = connectionWithSameCredentials && !connection.supportsReAuth - if (allowStickyConnection !== true && (shouldCreateStickyConnection || connection._sticky)) { + if (shouldCreateStickyConnection || connection._sticky) { await connection._release() throw newError('Driver is connected to a database that does not support user switch.') - } else if (allowStickyConnection === true && shouldCreateStickyConnection) { - await connection._release() - connection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) - connection._sticky = true - return connection - } else if (connection._sticky) { - return connection } } diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index 1bc82587c..65e077100 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -136,7 +136,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { let name let address const context = { database: database || DEFAULT_DB_NAME } @@ -155,7 +155,6 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, auth, - allowStickyConnection, onDatabaseNameResolved: (databaseName) => { context.database = context.database || databaseName if (onDatabaseNameResolved) { @@ -187,15 +186,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const connection = await this._connectionPool.acquire({ auth }, address) if (auth) { - const stickyConnection = await this._getStickyConnection({ + await this._verifyStickyConnection({ auth, connection, - address, - allowStickyConnection + address }) - if (stickyConnection) { - return stickyConnection - } return connection } @@ -275,9 +270,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - async verifyAuthentication ({ database, accessMode, auth, allowStickyConnection }) { + async verifyAuthentication ({ database, accessMode, auth }) { return this._verifyAuthentication({ - allowStickyConnection, auth, getAddress: async () => { const context = { database: database || DEFAULT_DB_NAME } @@ -286,7 +280,6 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider accessMode, database: context.database, auth, - allowStickyConnection, onDatabaseNameResolved: (databaseName) => { context.database = context.database || databaseName } @@ -351,7 +344,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection } = {}) { + _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { const currentRoutingTable = this._routingTableRegistry.get( database, () => new RoutingTable({ database }) @@ -363,10 +356,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._log.info( `Routing table is stale for database: "${database}" and access mode: "${accessMode}": ${currentRoutingTable}` ) - return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection) + return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) } - _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection) { + _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) { const knownRouters = currentRoutingTable.routers if (this._useSeedRouter) { @@ -376,8 +369,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth, - allowStickyConnection + auth ) } return this._fetchRoutingTableFromKnownRoutersFallbackToSeedRouter( @@ -386,8 +378,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth, - allowStickyConnection + auth ) } @@ -397,8 +388,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth, - allowStickyConnection + auth ) { // we start with seed router, no routers were probed before const seenRouters = [] @@ -408,8 +398,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) if (newRoutingTable) { @@ -421,8 +410,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) newRoutingTable = newRoutingTable2 error = error2 || error @@ -442,16 +430,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth, - allowStickyConnection + auth ) { let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters( knownRouters, currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) if (!newRoutingTable) { @@ -462,8 +448,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) } @@ -480,16 +465,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) { const [newRoutingTable, error] = await this._fetchRoutingTable( knownRouters, currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) if (newRoutingTable) { @@ -515,8 +498,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider routingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) { const resolvedAddresses = await this._resolveSeedRouter(seedRouter) @@ -525,7 +507,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider address => seenRouters.indexOf(address) < 0 ) - return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth, allowStickyConnection) + return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth) } async _resolveSeedRouter (seedRouter) { @@ -537,7 +519,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return [].concat.apply([], dnsResolvedAddresses) } - async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth, allowStickyConnection) { + async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth) { return routerAddresses.reduce( async (refreshedTablePromise, currentRouter, currentIndex) => { const [newRoutingTable] = await refreshedTablePromise @@ -561,8 +543,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRouter, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) if (session) { try { @@ -587,20 +568,16 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth, allowStickyConnection) { + async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth) { try { - let connection = await this._connectionPool.acquire({ auth }, routerAddress) + const connection = await this._connectionPool.acquire({ auth }, routerAddress) if (auth) { - const stickyConnection = await this._getStickyConnection({ + await this._verifyStickyConnection({ auth, connection, - address: routerAddress, - allowStickyConnection + address: routerAddress }) - if (stickyConnection) { - connection = stickyConnection - } } const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index 79a0f04d7..bc8c52ca2 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -504,11 +504,7 @@ describe('constructor', () => { }) describe('user-switching', () => { - describe.each([ - undefined, - false, - null - ])('when allowStickyConnection is %s', (allowStickyConnection) => { + describe('should not allow sticky connections', () => { describe('when does not supports re-auth', () => { it.each([ ['new connection', { other: 'auth' }, { other: 'auth' }, true], @@ -524,7 +520,6 @@ describe('user-switching', () => { .acquireConnection({ accessMode: 'READ', database: '', - allowStickyConnection, auth: acquireAuth }) .catch(functional.identity) @@ -551,73 +546,6 @@ describe('user-switching', () => { .acquireConnection({ accessMode: 'READ', database: '', - allowStickyConnection, - auth: acquireAuth - }) - - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) - }) - }) - }) - - describe.each([ - true - ])('when allowStickyConnection is %s', (allowStickyConnection) => { - describe('when does not supports re-auth', () => { - it.each([ - ['new connection', { other: 'auth' }, { other: 'auth' }, false], - ['old connection', { some: 'auth' }, { other: 'token' }, true] - ])('should raise and error when try switch user on acquire [%s]', async (_, connAuth, acquireAuth, shouldCreateNew) => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth) - const connection2 = new FakeConnection(address, () => {}, undefined, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire') - .mockResolvedValueOnce(connection) - .mockResolvedValueOnce(connection2) - const connectionProvider = newDirectConnectionProvider(address, pool) - - const acquiredConnection = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: '', - allowStickyConnection, - auth: acquireAuth - }) - - expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address) - - if (shouldCreateNew) { - expect(connection._release).toHaveBeenCalled() - expect(connection._sticky).toBe(false) - expect(acquiredConnection).toEqual(connection2) - expect(poolAcquire).toHaveBeenCalledWith({ auth: acquireAuth }, address, { requireNew: true }) - } else { - expect(acquiredConnection).toEqual(connection) - } - - expect(acquiredConnection._release).not.toHaveBeenCalled() - expect(acquiredConnection._sticky).toEqual(true) - }) - }) - - describe('when supports re-auth', () => { - const connAuth = { some: 'auth' } - const acquireAuth = connAuth - - it('should return connection when try switch user on acquire', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => {}, undefined, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newDirectConnectionProvider(address, pool) - - const acquiredConnection = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: '', - allowStickyConnection, auth: acquireAuth }) diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 92a85d3e1..1d325a14f 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -3384,177 +3384,16 @@ describe.each([ }) describe('user-switching', () => { - describe.each([ - undefined, - false, - null - ])('when allowStickyConnection is %s', (allowStickyConnection) => { - describe('when does not support re-auth', () => { - describe.each([ - ['new connection', { other: 'auth' }, { other: 'auth' }, true], - ['old connection', { some: 'auth' }, { other: 'token' }, false] - ])('%s', (_, connAuth, acquireAuth, isStickyConn) => { - it('should raise and error when try switch user on acquire', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProvider([ - newRoutingTable( - null, - [server1, server2], - [server3, server2], - [server2, server4] - ) - ], - pool - ) - const auth = acquireAuth - - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: '', - allowStickyConnection, - auth - }) - .catch(functional.identity) - - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) - expect(connection._release).toHaveBeenCalled() - expect(connection._sticky).toEqual(isStickyConn) - }) - - it('should raise and error when try switch user on acquire [expired rt]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProvider([ - newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4], - int(0) // expired - ) - ], - pool, - { - dba: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) - } - ) - connectionProvider._useSeedRouter = false - - const auth = acquireAuth - - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) - .catch(functional.identity) - - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) - expect(connection._release).toHaveBeenCalled() - expect(connection._sticky).toEqual(isStickyConn) - }) - - it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProviderWithSeedRouter( - server0, - [server0], - [ - newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4], - int(0) // expired - ) - ], - { - dba: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) - }, - pool - ) - - const auth = acquireAuth - - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) - .catch(functional.identity) - - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) - expect(connection._release).toHaveBeenCalled() - expect(connection._sticky).toEqual(isStickyConn) - }) - - it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) - const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProviderWithSeedRouter( - server0, - [server0], - [], - {}, - pool - ) - - const auth = acquireAuth - - const error = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) - .catch(functional.identity) - - expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) - expect(connection._release).toHaveBeenCalled() - expect(connection._sticky).toEqual(isStickyConn) - }) - }) - }) - - describe('when does it support re-auth', () => { - const connAuth = { myAuth: 'auth' } - const acquireAuth = connAuth - - it('should return connection when try switch user on acquire', async () => { + describe('when does not support re-auth', () => { + describe.each([ + ['new connection', { other: 'auth' }, { other: 'auth' }, true], + ['old connection', { some: 'auth' }, { other: 'token' }, false] + ])('%s', (_, connAuth, acquireAuth, isStickyConn) => { + it('should raise and error when try switch user on acquire', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newRoutingConnectionProvider([ newRoutingTable( null, @@ -3567,23 +3406,25 @@ describe.each([ ) const auth = acquireAuth - const acquiredConnection = await connectionProvider + const error = await connectionProvider .acquireConnection({ accessMode: 'READ', database: '', - allowStickyConnection, auth }) + .catch(functional.identity) - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) + expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) }) - it('should return connection when try switch user on acquire [expired rt]', async () => { + it('should raise and error when try switch user on acquire [expired rt]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newRoutingConnectionProvider([ newRoutingTable( 'dba', @@ -3595,37 +3436,37 @@ describe.each([ ], pool, { - dba: { - [server1.asHostPort()]: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) - } + dba: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) } ) connectionProvider._useSeedRouter = false const auth = acquireAuth - const acquiredConnection = await connectionProvider + const error = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', - allowStickyConnection, auth }) + .catch(functional.identity) - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) + expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) }) - it('should return connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { + it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newRoutingConnectionProviderWithSeedRouter( server0, [server0], @@ -3639,356 +3480,145 @@ describe.each([ ) ], { - dba: { - [server0.asHostPort()]: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) - } + dba: newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4] + ) }, pool ) const auth = acquireAuth - const acquiredConnection = await connectionProvider + const error = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', - allowStickyConnection, auth }) + .catch(functional.identity) - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) }) - it('should not delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { + it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) + const poolAcquire = jest.spyOn(pool, 'acquire').mockResolvedValue(connection) const connectionProvider = newRoutingConnectionProviderWithSeedRouter( server0, [server0], [], - { - dba: { - [server0.asHostPort()]: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) - } - }, + {}, pool ) const auth = acquireAuth - const acquiredConnection = await connectionProvider + const error = await connectionProvider .acquireConnection({ accessMode: 'READ', database: 'dba', - allowStickyConnection, auth }) + .catch(functional.identity) - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) + expect(error).toEqual(newError('Driver is connected to a database that does not support user switch.')) + expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) + expect(connection._release).toHaveBeenCalled() + expect(connection._sticky).toEqual(isStickyConn) }) }) }) - describe.each([ - true - ])('when allowStickyConnection is %s', (allowStickyConnection) => { - describe('when does not support re-auth', () => { - describe.each([ - ['new connection', { other: 'auth' }, { other: 'auth' }, false], - ['old connection', { some: 'auth' }, { other: 'token' }, true] - ])('%s', (_, connAuth, acquireAuth, shouldCreateNew) => { - it('should raise and error when try switch user on acquire', async () => { - const pool = newPool() - const createdConnections = {} - const poolAcquire = jest.spyOn(pool, 'acquire') - .mockImplementation((_, address) => { - const conn = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) - createdConnections[address.asKey()] = [...createdConnections[address.asKey()] || [], conn] - return conn - }) - const connectionProvider = newRoutingConnectionProvider([ - newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) - ], - pool - ) - const auth = acquireAuth - - const acquiredConnection = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) - - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3) - const connections = createdConnections[server3.asKey()] - if (shouldCreateNew) { - // discarded connection should not be marked as sticky and release - expect(connections[0]._release).toHaveBeenCalled() - expect(connections[0]._sticky).toBe(false) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server3, { requireNew: true }) - } else { - expect(poolAcquire).not.toHaveBeenCalledWith({ auth }, server3, { requireNew: true }) - } - - // used connection should be marked as sticky - expect(connections[connections.length - 1]._release).not.toHaveBeenCalled() - expect(connections[connections.length - 1]._sticky).toBe(true) - - expect(acquiredConnection).toBe(connections[connections.length - 1]) - expect(acquiredConnection._release).not.toHaveBeenCalled() - expect(acquiredConnection._sticky).toEqual(true) - }) - - it('should raise and error when try switch user on acquire [expired rt]', async () => { - const pool = newPool() - const createdConnections = {} - const database = 'dba' - const poolAcquire = jest.spyOn(pool, 'acquire') - .mockImplementation((_, address) => { - const conn = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) - createdConnections[address.asKey()] = [...createdConnections[address.asKey()] || [], conn] - return conn - }) - const connectionProvider = newRoutingConnectionProvider([ - newRoutingTable( - database, - [server1, server2], - [server3, server2], - [server2, server4], - int(0) // expired - ) - ], - pool, - { - [database]: { - [server1.asKey()]: newRoutingTable( - database, - [server1, server2], - [server3, server2], - [server2, server4] - ) - } - }) - connectionProvider._useSeedRouter = false - - const auth = acquireAuth - - await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database, - allowStickyConnection, - auth - }) + describe('when does it support re-auth', () => { + const connAuth = { myAuth: 'auth' } + const acquireAuth = connAuth - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1) - const connections = createdConnections[server1.asKey()] - if (shouldCreateNew) { - // discarded connection should not be marked as sticky and release - expect(connections[0]._release).toHaveBeenCalled() - expect(connections[0]._sticky).toBe(false) - expect(connections.length).toBe(2) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server1, { requireNew: true }) - } else { - expect(connections.length).toBe(1) - expect(poolAcquire).not.toHaveBeenCalledWith({ auth }, server1, { requireNew: true }) - } - - // used connection should be marked as sticky and released - expect(connections[connections.length - 1]._sticky).toBe(true) - // the release will be done by the session.close(), - // since the rediscovery is mocked, the session doesn't acquire the connection. - // thus, never release the connection. - expect(connections[connections.length - 1]._release).not.toHaveBeenCalled() - }) - - it('should raise and error when try switch user on acquire [expired rt and userSeedRouter]', async () => { - const pool = newPool() - const createdConnections = {} - const database = 'dba' - const poolAcquire = jest.spyOn(pool, 'acquire') - .mockImplementation((_, address) => { - const conn = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) - createdConnections[address.asKey()] = [...createdConnections[address.asKey()] || [], conn] - return conn - }) - - const connectionProvider = newRoutingConnectionProviderWithSeedRouter( - server0, - [server0], - [ - newRoutingTable( - database, - [server1, server2], - [server3, server2], - [server2, server4], - int(0) // expired - ) - ], - { - [database]: { - [server0.asKey()]: newRoutingTable( - database, - [server1, server2], - [server3, server2], - [server2, server4] - ) - } - }, - pool - ) - - const auth = acquireAuth - - await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database, - allowStickyConnection, - auth - }) - - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) - const connections = createdConnections[server0.asKey()] - if (shouldCreateNew) { - // discarded connection should not be marked as sticky and release - expect(connections[0]._release).toHaveBeenCalled() - expect(connections[0]._sticky).toBe(false) - expect(connections.length).toBe(2) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0, { requireNew: true }) - } else { - expect(connections.length).toBe(1) - expect(poolAcquire).not.toHaveBeenCalledWith({ auth }, server0, { requireNew: true }) - } + it('should return connection when try switch user on acquire', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + null, + [server1, server2], + [server3, server2], + [server2, server4] + ) + ], + pool + ) + const auth = acquireAuth - // used connection should be marked as sticky and released - expect(connections[connections.length - 1]._sticky).toBe(true) - // the release will be done by the session.close(), - // since the rediscovery is mocked, the session doesn't acquire the connection. - // thus, never release the connection. - expect(connections[connections.length - 1]._release).not.toHaveBeenCalled() + const acquiredConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: '', + auth }) - it('should raise and error when try switch user on acquire [firstCall and userSeedRouter]', async () => { - const pool = newPool() - const createdConnections = {} - const database = 'dba' - const poolAcquire = jest.spyOn(pool, 'acquire') - .mockImplementation((_, address) => { - const conn = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth) - createdConnections[address.asKey()] = [...createdConnections[address.asKey()] || [], conn] - return conn - }) - const connectionProvider = newRoutingConnectionProviderWithSeedRouter( - server0, - [server0], - [], - { - [database]: { - [server0.asKey()]: newRoutingTable( - database, - [server1, server2], - [server3, server2], - [server2, server4] - ) - } - }, - pool - ) - - const auth = acquireAuth - - await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database, - allowStickyConnection, - auth - }) - - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0) - const connections = createdConnections[server0.asKey()] - if (shouldCreateNew) { - // discarded connection should not be marked as sticky and release - expect(connections[0]._release).toHaveBeenCalled() - expect(connections[0]._sticky).toBe(false) - expect(connections.length).toBe(2) - expect(poolAcquire).toHaveBeenCalledWith({ auth }, server0, { requireNew: true }) - } else { - expect(connections.length).toBe(1) - expect(poolAcquire).not.toHaveBeenCalledWith({ auth }, server0, { requireNew: true }) - } - - // used connection should be marked as sticky and released - expect(connections[connections.length - 1]._sticky).toBe(true) - // the release will be done by the session.close(), - // since the rediscovery is mocked, the session doesn't acquire the connection. - // thus, never release the connection. - expect(connections[connections.length - 1]._release).not.toHaveBeenCalled() - }) - }) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) - describe('when does it support re-auth', () => { - const connAuth = { myAuth: 'auth' } - const acquireAuth = connAuth - - it('should return connection when try switch user on acquire', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProvider([ - newRoutingTable( - null, + it('should return connection when try switch user on acquire [expired rt]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProvider([ + newRoutingTable( + 'dba', + [server1, server2], + [server3, server2], + [server2, server4], + int(0) // expired + ) + ], + pool, + { + dba: { + [server1.asHostPort()]: newRoutingTable( + 'dba', [server1, server2], [server3, server2], [server2, server4] ) - ], - pool - ) - const auth = acquireAuth + } + } + ) + connectionProvider._useSeedRouter = false - const acquiredConnection = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: '', - allowStickyConnection, - auth - }) + const auth = acquireAuth - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) - }) + const acquiredConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + auth + }) - it('should return connection when try switch user on acquire [expired rt]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProvider([ + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) + }) + + it('should return connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [ newRoutingTable( 'dba', [server1, server2], @@ -3997,113 +3627,65 @@ describe.each([ int(0) // expired ) ], - pool, { dba: { - [server1.asHostPort()]: newRoutingTable( + [server0.asHostPort()]: newRoutingTable( 'dba', [server1, server2], [server3, server2], [server2, server4] ) } - } - ) - connectionProvider._useSeedRouter = false + }, + pool + ) - const auth = acquireAuth + const auth = acquireAuth - const acquiredConnection = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) + const acquiredConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + auth + }) - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) - }) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) + }) - it('should return connection when try switch user on acquire [expired rt and userSeedRouter]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProviderWithSeedRouter( - server0, - [server0], - [ - newRoutingTable( + it('should not delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { + const address = ServerAddress.fromUrl('localhost:123') + const pool = newPool() + const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) + jest.spyOn(pool, 'acquire').mockResolvedValue(connection) + const connectionProvider = newRoutingConnectionProviderWithSeedRouter( + server0, + [server0], + [], + { + dba: { + [server0.asHostPort()]: newRoutingTable( 'dba', [server1, server2], [server3, server2], - [server2, server4], - int(0) // expired + [server2, server4] ) - ], - { - dba: { - [server0.asHostPort()]: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) - } - }, - pool - ) - - const auth = acquireAuth - - const acquiredConnection = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) - - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) - }) - - it('should not delegated connection when try switch user on acquire [firstCall and userSeedRouter]', async () => { - const address = ServerAddress.fromUrl('localhost:123') - const pool = newPool() - const connection = new FakeConnection(address, () => { }, undefined, PROTOCOL_VERSION, null, connAuth, { supportsReAuth: true }) - jest.spyOn(pool, 'acquire').mockResolvedValue(connection) - const connectionProvider = newRoutingConnectionProviderWithSeedRouter( - server0, - [server0], - [], - { - dba: { - [server0.asHostPort()]: newRoutingTable( - 'dba', - [server1, server2], - [server3, server2], - [server2, server4] - ) - } - }, - pool - ) + } + }, + pool + ) - const auth = acquireAuth + const auth = acquireAuth - const acquiredConnection = await connectionProvider - .acquireConnection({ - accessMode: 'READ', - database: 'dba', - allowStickyConnection, - auth - }) + const acquiredConnection = await connectionProvider + .acquireConnection({ + accessMode: 'READ', + database: 'dba', + auth + }) - expect(acquiredConnection).toBe(connection) - expect(acquiredConnection._sticky).toEqual(false) - }) + expect(acquiredConnection).toBe(connection) + expect(acquiredConnection._sticky).toEqual(false) }) }) }) diff --git a/packages/core/src/connection-provider.ts b/packages/core/src/connection-provider.ts index c5f2b9df2..bd0c96f03 100644 --- a/packages/core/src/connection-provider.ts +++ b/packages/core/src/connection-provider.ts @@ -53,7 +53,6 @@ class ConnectionProvider { impersonatedUser?: string onDatabaseNameResolved?: (databaseName?: string) => void auth?: AuthToken - allowStickyConnection?: boolean }): Promise { throw Error('Not implemented') } @@ -120,12 +119,11 @@ class ConnectionProvider { * @property {AuthToken} param.auth - the target auth for the to-be-acquired connection * @property {string} param.database - the target database for the to-be-acquired connection * @property {string} param.accessMode - the access mode for the to-be-acquired connection - * @property {boolean} param.allowStickyConnection - enables the usage of sticky connection for backwards compatibility * * @returns {Promise} promise resolved with true if succeed, false if failed with * authentication issue and rejected with error if non-authentication error happens. */ - verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string, allowStickyConnection?: boolean }): Promise { + verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string }): Promise { throw Error('Not implemented') } diff --git a/packages/core/src/internal/connection-holder.ts b/packages/core/src/internal/connection-holder.ts index 3ba2eafe6..76dda3abf 100644 --- a/packages/core/src/internal/connection-holder.ts +++ b/packages/core/src/internal/connection-holder.ts @@ -87,7 +87,6 @@ class ConnectionHolder implements ConnectionHolderInterface { private readonly _getConnectionAcquistionBookmarks: () => Promise private readonly _onDatabaseNameResolved?: (databaseName?: string) => void private readonly _auth?: AuthToken - private readonly _backwardsCompatibleAuth?: boolean private _closed: boolean /** @@ -101,7 +100,6 @@ class ConnectionHolder implements ConnectionHolderInterface { * @property {function(databaseName:string)} params.onDatabaseNameResolved - callback called when the database name is resolved * @property {function():Promise} params.getConnectionAcquistionBookmarks - called for getting Bookmarks for acquiring connections * @property {AuthToken} params.auth - the target auth for the to-be-acquired connection - * @property {boolean} params.backwardsCompatibleAuth - Enables backwards compatible re-auth */ constructor ({ mode = ACCESS_MODE_WRITE, @@ -111,8 +109,7 @@ class ConnectionHolder implements ConnectionHolderInterface { impersonatedUser, onDatabaseNameResolved, getConnectionAcquistionBookmarks, - auth, - backwardsCompatibleAuth + auth }: { mode?: string database?: string @@ -122,7 +119,6 @@ class ConnectionHolder implements ConnectionHolderInterface { onDatabaseNameResolved?: (databaseName?: string) => void getConnectionAcquistionBookmarks?: () => Promise auth?: AuthToken - backwardsCompatibleAuth?: boolean } = {}) { this._mode = mode this._closed = false @@ -134,7 +130,6 @@ class ConnectionHolder implements ConnectionHolderInterface { this._connectionPromise = Promise.resolve(null) this._onDatabaseNameResolved = onDatabaseNameResolved this._auth = auth - this._backwardsCompatibleAuth = backwardsCompatibleAuth this._getConnectionAcquistionBookmarks = getConnectionAcquistionBookmarks ?? (() => Promise.resolve(Bookmarks.empty())) } @@ -180,8 +175,7 @@ class ConnectionHolder implements ConnectionHolderInterface { bookmarks: await this._getBookmarks(), impersonatedUser: this._impersonatedUser, onDatabaseNameResolved: this._onDatabaseNameResolved, - auth: this._auth, - allowStickyConnection: this._backwardsCompatibleAuth + auth: this._auth }) } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 255ae5301..80ee7b24a 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -86,6 +86,7 @@ class Session { * @param {boolean} args.reactive - Whether this session should create reactive streams * @param {number} args.fetchSize - Defines how many records is pulled in each pulling batch * @param {string} args.impersonatedUser - The username which the user wants to impersonate for the duration of the session. + * @param {AuthToken} args.auth - the target auth for the to-be-acquired connection * @param {NotificationFilter} args.notificationFilter - The notification filter used for this session. */ constructor ({ diff --git a/packages/core/test/driver.test.ts b/packages/core/test/driver.test.ts index 062aabab5..e13a77142 100644 --- a/packages/core/test/driver.test.ts +++ b/packages/core/test/driver.test.ts @@ -617,8 +617,7 @@ describe('Driver', () => { fetchSize: 1000, maxConnectionLifetime: 3600000, maxConnectionPoolSize: 100, - connectionTimeout: 30000, - backwardsCompatibleAuth: false + connectionTimeout: 30000 }, connectionProvider, database: '', @@ -626,7 +625,6 @@ describe('Driver', () => { mode: 'WRITE', reactive: false, impersonatedUser: undefined, - backwardsCompatibleAuth: false, ...extra } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js index facd5c4bc..bc9ddb666 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-direct.js @@ -47,7 +47,7 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, auth, allowStickyConnection, forceReAuth } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, auth, forceReAuth } = {}) { const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ errorCode: SERVICE_UNAVAILABLE, handleAuthorizationExpired: (error, address, conn) => @@ -57,15 +57,11 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { const connection = await this._connectionPool.acquire({ auth, forceReAuth }, this._address) if (auth) { - const stickyConnection = await this._getStickyConnection({ + await this._verifyStickyConnection({ auth, connection, - address: this._address, - allowStickyConnection + address: this._address }) - if (stickyConnection) { - return stickyConnection - } return connection } @@ -132,9 +128,8 @@ export default class DirectConnectionProvider extends PooledConnectionProvider { ) } - async verifyAuthentication ({ auth, allowStickyConnection }) { + async verifyAuthentication ({ auth }) { return this._verifyAuthentication({ - allowStickyConnection, auth, getAddress: () => this._address }) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js index 8de11a907..0b0fc5bfa 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-pooled.js @@ -164,7 +164,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { return serverInfo } - async _verifyAuthentication ({ getAddress, auth, allowStickyConnection }) { + async _verifyAuthentication ({ getAddress, auth }) { const connectionsToRelease = [] try { const address = await getAddress() @@ -173,7 +173,7 @@ export default class PooledConnectionProvider extends ConnectionProvider { const lastMessageIsNotLogin = !connection.protocol().isLastMessageLogon() - if (!connection.supportsReAuth && !allowStickyConnection) { + if (!connection.supportsReAuth) { throw newError('Driver is connected to a database that does not support user switch.') } if (lastMessageIsNotLogin && connection.supportsReAuth) { @@ -194,21 +194,14 @@ export default class PooledConnectionProvider extends ConnectionProvider { } } - async _getStickyConnection ({ auth, connection, address, allowStickyConnection }) { + async _verifyStickyConnection ({ auth, connection, address }) { const connectionWithSameCredentials = object.equals(auth, connection.authToken) const shouldCreateStickyConnection = !connectionWithSameCredentials connection._sticky = connectionWithSameCredentials && !connection.supportsReAuth - if (allowStickyConnection !== true && (shouldCreateStickyConnection || connection._sticky)) { + if (shouldCreateStickyConnection || connection._sticky) { await connection._release() throw newError('Driver is connected to a database that does not support user switch.') - } else if (allowStickyConnection === true && shouldCreateStickyConnection) { - await connection._release() - connection = await this._connectionPool.acquire({ auth }, address, { requireNew: true }) - connection._sticky = true - return connection - } else if (connection._sticky) { - return connection } } diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js index d79bd9de5..c66d09b97 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/connection-provider-routing.js @@ -136,7 +136,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider * See {@link ConnectionProvider} for more information about this method and * its arguments. */ - async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection } = {}) { + async acquireConnection ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { let name let address const context = { database: database || DEFAULT_DB_NAME } @@ -155,7 +155,6 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, auth, - allowStickyConnection, onDatabaseNameResolved: (databaseName) => { context.database = context.database || databaseName if (onDatabaseNameResolved) { @@ -187,15 +186,11 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const connection = await this._connectionPool.acquire({ auth }, address) if (auth) { - const stickyConnection = await this._getStickyConnection({ + await this._verifyStickyConnection({ auth, connection, - address, - allowStickyConnection + address }) - if (stickyConnection) { - return stickyConnection - } return connection } @@ -275,9 +270,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - async verifyAuthentication ({ database, accessMode, auth, allowStickyConnection }) { + async verifyAuthentication ({ database, accessMode, auth }) { return this._verifyAuthentication({ - allowStickyConnection, auth, getAddress: async () => { const context = { database: database || DEFAULT_DB_NAME } @@ -286,7 +280,6 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider accessMode, database: context.database, auth, - allowStickyConnection, onDatabaseNameResolved: (databaseName) => { context.database = context.database || databaseName } @@ -351,7 +344,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider }) } - _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection } = {}) { + _freshRoutingTable ({ accessMode, database, bookmarks, impersonatedUser, onDatabaseNameResolved, auth } = {}) { const currentRoutingTable = this._routingTableRegistry.get( database, () => new RoutingTable({ database }) @@ -363,10 +356,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._log.info( `Routing table is stale for database: "${database}" and access mode: "${accessMode}": ${currentRoutingTable}` ) - return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection) + return this._refreshRoutingTable(currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) } - _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth, allowStickyConnection) { + _refreshRoutingTable (currentRoutingTable, bookmarks, impersonatedUser, onDatabaseNameResolved, auth) { const knownRouters = currentRoutingTable.routers if (this._useSeedRouter) { @@ -376,8 +369,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth, - allowStickyConnection + auth ) } return this._fetchRoutingTableFromKnownRoutersFallbackToSeedRouter( @@ -386,8 +378,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth, - allowStickyConnection + auth ) } @@ -397,8 +388,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth, - allowStickyConnection + auth ) { // we start with seed router, no routers were probed before const seenRouters = [] @@ -408,8 +398,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) if (newRoutingTable) { @@ -421,8 +410,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) newRoutingTable = newRoutingTable2 error = error2 || error @@ -442,16 +430,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser, onDatabaseNameResolved, - auth, - allowStickyConnection + auth ) { let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters( knownRouters, currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) if (!newRoutingTable) { @@ -462,8 +448,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) } @@ -480,16 +465,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) { const [newRoutingTable, error] = await this._fetchRoutingTable( knownRouters, currentRoutingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) if (newRoutingTable) { @@ -515,8 +498,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider routingTable, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) { const resolvedAddresses = await this._resolveSeedRouter(seedRouter) @@ -525,7 +507,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider address => seenRouters.indexOf(address) < 0 ) - return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth, allowStickyConnection) + return await this._fetchRoutingTable(newAddresses, routingTable, bookmarks, impersonatedUser, auth) } async _resolveSeedRouter (seedRouter) { @@ -537,7 +519,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return [].concat.apply([], dnsResolvedAddresses) } - async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth, allowStickyConnection) { + async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser, auth) { return routerAddresses.reduce( async (refreshedTablePromise, currentRouter, currentIndex) => { const [newRoutingTable] = await refreshedTablePromise @@ -561,8 +543,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider currentRouter, bookmarks, impersonatedUser, - auth, - allowStickyConnection + auth ) if (session) { try { @@ -587,20 +568,16 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) } - async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth, allowStickyConnection) { + async _createSessionForRediscovery (routerAddress, bookmarks, impersonatedUser, auth) { try { - let connection = await this._connectionPool.acquire({ auth }, routerAddress) + const connection = await this._connectionPool.acquire({ auth }, routerAddress) if (auth) { - const stickyConnection = await this._getStickyConnection({ + await this._verifyStickyConnection({ auth, connection, - address: routerAddress, - allowStickyConnection + address: routerAddress }) - if (stickyConnection) { - connection = stickyConnection - } } const databaseSpecificErrorHandler = ConnectionErrorHandler.create({ diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts index 6d88f65b3..70dd99811 100644 --- a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -30,10 +30,10 @@ import { util } from './internal/index.ts' export default class AuthTokenManager { /** * Returns a valid token. - * + * * **Warning**: This method must only ever return auth information belonging to the same identity. * Switching identities using the `AuthTokenManager` is undefined behavior. - * + * * @returns {Promise|AuthToken} The valid auth token or a promise for a valid auth token */ getToken (): Promise | AuthToken { @@ -42,9 +42,9 @@ export default class AuthTokenManager { /** * Called to notify a token expiration. - * + * * @param {AuthToken} token The expired token. - * @return {void} + * @return {void} */ onTokenExpired (token: AuthToken): void { throw new Error('Not implemented') @@ -87,12 +87,12 @@ export class TemporalAuthData { /** * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. - * + * * **Warning**: `getAuthData` must only ever return auth information belonging to the same identity. - * Switching identities using the `AuthTokenManager` is undefined behavior. + * Switching identities using the `AuthTokenManager` is undefined behavior. * * @param {object} param0 - The params - * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token. + * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token. * Must only ever return auth information belonging to the same identity. * @returns {AuthTokenManager} The temporal auth data manager. * @experimental Exposed as preview feature. diff --git a/packages/neo4j-driver-deno/lib/core/connection-provider.ts b/packages/neo4j-driver-deno/lib/core/connection-provider.ts index a03a86fb4..5de1b320d 100644 --- a/packages/neo4j-driver-deno/lib/core/connection-provider.ts +++ b/packages/neo4j-driver-deno/lib/core/connection-provider.ts @@ -53,7 +53,6 @@ class ConnectionProvider { impersonatedUser?: string onDatabaseNameResolved?: (databaseName?: string) => void auth?: AuthToken - allowStickyConnection?: boolean }): Promise { throw Error('Not implemented') } @@ -120,12 +119,11 @@ class ConnectionProvider { * @property {AuthToken} param.auth - the target auth for the to-be-acquired connection * @property {string} param.database - the target database for the to-be-acquired connection * @property {string} param.accessMode - the access mode for the to-be-acquired connection - * @property {boolean} param.allowStickyConnection - enables the usage of sticky connection for backwards compatibility * * @returns {Promise} promise resolved with true if succeed, false if failed with * authentication issue and rejected with error if non-authentication error happens. */ - verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string, allowStickyConnection?: boolean }): Promise { + verifyAuthentication (param?: { auth?: AuthToken, database?: string, accessMode?: string }): Promise { throw Error('Not implemented') } diff --git a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts index 967266ca2..7e49502ec 100644 --- a/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts +++ b/packages/neo4j-driver-deno/lib/core/internal/connection-holder.ts @@ -87,7 +87,6 @@ class ConnectionHolder implements ConnectionHolderInterface { private readonly _getConnectionAcquistionBookmarks: () => Promise private readonly _onDatabaseNameResolved?: (databaseName?: string) => void private readonly _auth?: AuthToken - private readonly _backwardsCompatibleAuth?: boolean private _closed: boolean /** @@ -101,7 +100,6 @@ class ConnectionHolder implements ConnectionHolderInterface { * @property {function(databaseName:string)} params.onDatabaseNameResolved - callback called when the database name is resolved * @property {function():Promise} params.getConnectionAcquistionBookmarks - called for getting Bookmarks for acquiring connections * @property {AuthToken} params.auth - the target auth for the to-be-acquired connection - * @property {boolean} params.backwardsCompatibleAuth - Enables backwards compatible re-auth */ constructor ({ mode = ACCESS_MODE_WRITE, @@ -111,8 +109,7 @@ class ConnectionHolder implements ConnectionHolderInterface { impersonatedUser, onDatabaseNameResolved, getConnectionAcquistionBookmarks, - auth, - backwardsCompatibleAuth + auth }: { mode?: string database?: string @@ -122,7 +119,6 @@ class ConnectionHolder implements ConnectionHolderInterface { onDatabaseNameResolved?: (databaseName?: string) => void getConnectionAcquistionBookmarks?: () => Promise auth?: AuthToken - backwardsCompatibleAuth?: boolean } = {}) { this._mode = mode this._closed = false @@ -134,7 +130,6 @@ class ConnectionHolder implements ConnectionHolderInterface { this._connectionPromise = Promise.resolve(null) this._onDatabaseNameResolved = onDatabaseNameResolved this._auth = auth - this._backwardsCompatibleAuth = backwardsCompatibleAuth this._getConnectionAcquistionBookmarks = getConnectionAcquistionBookmarks ?? (() => Promise.resolve(Bookmarks.empty())) } @@ -180,8 +175,7 @@ class ConnectionHolder implements ConnectionHolderInterface { bookmarks: await this._getBookmarks(), impersonatedUser: this._impersonatedUser, onDatabaseNameResolved: this._onDatabaseNameResolved, - auth: this._auth, - allowStickyConnection: this._backwardsCompatibleAuth + auth: this._auth }) } diff --git a/packages/neo4j-driver-deno/lib/core/session.ts b/packages/neo4j-driver-deno/lib/core/session.ts index 246d7e0dd..051045979 100644 --- a/packages/neo4j-driver-deno/lib/core/session.ts +++ b/packages/neo4j-driver-deno/lib/core/session.ts @@ -86,6 +86,7 @@ class Session { * @param {boolean} args.reactive - Whether this session should create reactive streams * @param {number} args.fetchSize - Defines how many records is pulled in each pulling batch * @param {string} args.impersonatedUser - The username which the user wants to impersonate for the duration of the session. + * @param {AuthToken} args.auth - the target auth for the to-be-acquired connection * @param {NotificationFilter} args.notificationFilter - The notification filter used for this session. */ constructor ({ diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index e0d55b438..cc471fdce 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -14,8 +14,7 @@ export function NewDriver ({ neo4j }, context, data, wire) { authorizationToken, authTokenManagerId, userAgent, - resolverRegistered, - backwardsCompatibleAuth + resolverRegistered } = data let parsedAuthToken = null @@ -41,7 +40,6 @@ export function NewDriver ({ neo4j }, context, data, wire) { userAgent, resolver, useBigInt: true, - backwardsCompatibleAuth, logging: neo4j.logging.console(context.logLevel || context.environmentLogLevel) } if ('encrypted' in data) { From 78c2970dbe99156963c64e2882ff202f2671d6e6 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Mon, 3 Apr 2023 15:29:21 +0200 Subject: [PATCH 61/70] Fix rebase issues --- .../src/bolt/bolt-protocol-v1.js | 2 +- .../bolt-connection/bolt/bolt-protocol-v1.js | 2 +- packages/testkit-backend/package-lock.json | 311 +++++++++++++++++- .../testkit-backend/src/request-handlers.js | 3 - 4 files changed, 312 insertions(+), 6 deletions(-) diff --git a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js index 1ef3c169a..87b736ce7 100644 --- a/packages/bolt-connection/src/bolt/bolt-protocol-v1.js +++ b/packages/bolt-connection/src/bolt/bolt-protocol-v1.js @@ -236,7 +236,7 @@ export default class BoltProtocol { // TODO: Verify the Neo4j version in the message const error = newError( - 'Driver is connected to a database that does not support login. ' + + 'Driver is connected to a database that does not support logon. ' + 'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.' ) diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js index 4f7884652..1212bb019 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/bolt/bolt-protocol-v1.js @@ -236,7 +236,7 @@ export default class BoltProtocol { // TODO: Verify the Neo4j version in the message const error = newError( - 'Driver is connected to a database that does not support login. ' + + 'Driver is connected to a database that does not support logon. ' + 'Please upgrade to Neo4j 5.5.0 or later in order to use this functionality.' ) diff --git a/packages/testkit-backend/package-lock.json b/packages/testkit-backend/package-lock.json index 3bff0c0dd..202b0a406 100644 --- a/packages/testkit-backend/package-lock.json +++ b/packages/testkit-backend/package-lock.json @@ -18,7 +18,8 @@ "esm": "^3.2.25", "rollup": "^2.77.4-1", "rollup-plugin-inject-process-env": "^1.3.1", - "rollup-plugin-polyfill-node": "^0.11.0" + "rollup-plugin-polyfill-node": "^0.11.0", + "sinon": "^15.0.1" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -91,6 +92,59 @@ "rollup": "^1.20.0||^2.0.0" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0" + } + }, + "node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -169,6 +223,15 @@ "node": ">=0.10.0" } }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/esm": { "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", @@ -242,6 +305,15 @@ "node": ">= 0.4.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -300,6 +372,24 @@ "@types/estree": "*" } }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -337,6 +427,28 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, + "node_modules/nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/node-static": { "version": "0.7.11", "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz", @@ -386,6 +498,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -515,12 +636,51 @@ "node": ">=12" } }, + "node_modules/sinon": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.3.tgz", + "integrity": "sha512-si3geiRkeovP7Iel2O+qGL4NrO9vbMf3KsrJEi0ghP1l5aBkB5UxARea5j0FUsSqH3HLBh0dQPAyQ8fObRUqHw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "dev": true }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", @@ -611,6 +771,63 @@ "picomatch": "^2.2.2" } }, + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", + "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -677,6 +894,12 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true + }, "esm": { "version": "3.2.25", "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", @@ -731,6 +954,12 @@ "function-bind": "^1.1.1" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -780,6 +1009,24 @@ "@types/estree": "*" } }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "magic-string": { "version": "0.25.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", @@ -808,6 +1055,30 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, + "nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, "node-static": { "version": "0.7.11", "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz", @@ -848,6 +1119,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -938,12 +1218,41 @@ } } }, + "sinon": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.3.tgz", + "integrity": "sha512-si3geiRkeovP7Iel2O+qGL4NrO9vbMf3KsrJEi0ghP1l5aBkB5UxARea5j0FUsSqH3HLBh0dQPAyQ8fObRUqHw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + } + }, "sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "dev": true }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index cc471fdce..3a69409eb 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -121,7 +121,6 @@ export function NewSession ({ neo4j }, context, data, wire) { return } } -<<<<<<< HEAD let notificationFilter if ('notificationsMinSeverity' in data || 'notificationsDisabledCategories' in data) { notificationFilter = { @@ -129,12 +128,10 @@ export function NewSession ({ neo4j }, context, data, wire) { disabledCategories: data.notificationsDisabledCategories } } -======= const auth = data.authorizationToken != null ? context.binder.parseAuthToken(data.authorizationToken.data) : undefined ->>>>>>> 7fa91180 (Session config) const driver = context.getDriver(driverId) const session = driver.session({ defaultAccessMode: accessMode, From e9114113f1e8f18d4169a9c66668e8fcf18a5763 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 4 Apr 2023 11:21:28 +0200 Subject: [PATCH 62/70] Rename TemporalAuthTokenManager to ExpirationBasedAuthTokenManager Also rename the structure with token and expiration. --- .../authentication-provider.js | 6 +-- .../authentication-provider.test.js | 8 ++-- .../connection-provider-direct.test.js | 4 +- .../connection-provider-routing.test.js | 4 +- packages/core/src/auth-token-manager.ts | 42 +++++++++---------- packages/core/src/index.ts | 8 ++-- .../authentication-provider.js | 6 +-- .../lib/core/auth-token-manager.ts | 42 +++++++++---------- packages/neo4j-driver-deno/lib/core/index.ts | 8 ++-- packages/neo4j-driver-deno/lib/mod.ts | 10 ++--- packages/neo4j-driver-lite/src/index.ts | 10 ++--- packages/neo4j-driver/src/index.js | 6 +-- packages/neo4j-driver/types/index.d.ts | 10 ++--- .../src/request-handlers-rx.js | 4 +- .../testkit-backend/src/request-handlers.js | 30 ++++++------- packages/testkit-backend/src/responses.js | 4 +- 16 files changed, 101 insertions(+), 101 deletions(-) diff --git a/packages/bolt-connection/src/connection-provider/authentication-provider.js b/packages/bolt-connection/src/connection-provider/authentication-provider.js index 631f4aecb..7f406d127 100644 --- a/packages/bolt-connection/src/connection-provider/authentication-provider.js +++ b/packages/bolt-connection/src/connection-provider/authentication-provider.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import { temporalAuthDataManager } from 'neo4j-driver-core' +import { expirationBasedAuthTokenManager } from 'neo4j-driver-core' import { object } from '../lang' /** @@ -25,8 +25,8 @@ import { object } from '../lang' */ export default class AuthenticationProvider { constructor ({ authTokenManager, userAgent }) { - this._authTokenManager = authTokenManager || temporalAuthDataManager({ - getAuthData: () => {} + this._authTokenManager = authTokenManager || expirationBasedAuthTokenManager({ + tokenProvider: () => {} }) this._userAgent = userAgent } diff --git a/packages/bolt-connection/test/connection-provider/authentication-provider.test.js b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js index ab7538968..f9da1d3f4 100644 --- a/packages/bolt-connection/test/connection-provider/authentication-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { temporalAuthDataManager } from 'neo4j-driver-core' +import { expirationBasedAuthTokenManager } from 'neo4j-driver-core' import AuthenticationProvider from '../../src/connection-provider/authentication-provider' describe('AuthenticationProvider', () => { @@ -784,7 +784,7 @@ describe('AuthenticationProvider', () => { }) function createAuthenticationProvider (authTokenProvider, mocks) { - const authTokenManager = temporalAuthDataManager({ getAuthData: authTokenProvider }) + const authTokenManager = expirationBasedAuthTokenManager({ tokenProvider: authTokenProvider }) const provider = new AuthenticationProvider({ authTokenManager, userAgent: USER_AGENT @@ -807,10 +807,10 @@ describe('AuthenticationProvider', () => { return connection } - function toRenewableToken (token, expiry) { + function toRenewableToken (token, expiration) { return { token, - expiry + expiration } } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js index bc8c52ca2..83ff25abf 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js @@ -20,7 +20,7 @@ import DirectConnectionProvider from '../../src/connection-provider/connection-provider-direct' import { Pool } from '../../src/pool' import { Connection, DelegateConnection } from '../../src/connection' -import { internal, newError, ServerInfo, staticAuthTokenManager, temporalAuthDataManager } from 'neo4j-driver-core' +import { internal, newError, ServerInfo, staticAuthTokenManager, expirationBasedAuthTokenManager } from 'neo4j-driver-core' import AuthenticationProvider from '../../src/connection-provider/authentication-provider' import { functional } from '../../src/lang' @@ -209,7 +209,7 @@ it('should call authenticationAuthProvider.handleError when TokenExpired happens it('should change error to retriable when error when TokenExpired happens and staticAuthTokenManager is not being used', async () => { const address = ServerAddress.fromUrl('localhost:123') const pool = newPool() - const connectionProvider = newDirectConnectionProvider(address, pool, temporalAuthDataManager({ getAuthData: () => null })) + const connectionProvider = newDirectConnectionProvider(address, pool, expirationBasedAuthTokenManager({ tokenProvider: () => null })) const conn = await connectionProvider.acquireConnection({ accessMode: 'READ', diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index 1d325a14f..2ec3512a1 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -26,7 +26,7 @@ import { internal, ServerInfo, staticAuthTokenManager, - temporalAuthDataManager + expirationBasedAuthTokenManager } from 'neo4j-driver-core' import { RoutingTable } from '../../src/rediscovery/' import { Pool } from '../../src/pool' @@ -1718,7 +1718,7 @@ describe.each([ ], pool ) - connectionProvider._authTokenManager = temporalAuthDataManager({ getAuthData: () => null }) + connectionProvider._authTokenManager = expirationBasedAuthTokenManager({ tokenProvider: () => null }) const error = newError( 'Message', diff --git a/packages/core/src/auth-token-manager.ts b/packages/core/src/auth-token-manager.ts index 7fa2863b6..7e1d1c42c 100644 --- a/packages/core/src/auth-token-manager.ts +++ b/packages/core/src/auth-token-manager.ts @@ -52,14 +52,14 @@ export default class AuthTokenManager { } /** - * Interface which defines an {@link AuthToken} with an expiry data time associated + * Interface which defines an {@link AuthToken} with an expiration data time associated * @interface * @experimental Exposed as preview feature. * @since 5.7 */ -export class TemporalAuthData { +export class AuthTokenAndExpiration { public readonly token: AuthToken - public readonly expiry?: Date + public readonly expiration?: Date private constructor () { /** @@ -74,34 +74,34 @@ export class TemporalAuthData { * The expected expiration date of the auth token. * * This information will be used for triggering the auth token refresh - * in managers created with {@link temporalAuthDataManager}. + * in managers created with {@link expirationBasedAuthTokenManager}. * * If this value is not defined, the {@link AuthToken} will be considered valid * until a `Neo.ClientError.Security.TokenExpired` error happens. * * @type {Date|undefined} */ - this.expiry = undefined + this.expiration = undefined } } /** * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. * - * **Warning**: `getAuthData` must only ever return auth information belonging to the same identity. + * **Warning**: `tokenProvider` must only ever return auth information belonging to the same identity. * Switching identities using the `AuthTokenManager` is undefined behavior. * * @param {object} param0 - The params - * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token. + * @param {function(): Promise} param0.tokenProvider - Retrieves a new valid auth token. * Must only ever return auth information belonging to the same identity. * @returns {AuthTokenManager} The temporal auth data manager. * @experimental Exposed as preview feature. */ -export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => Promise }): AuthTokenManager { - if (typeof getAuthData !== 'function') { - throw new TypeError(`getAuthData should be function, but got: ${typeof getAuthData}`) +export function expirationBasedAuthTokenManager ({ tokenProvider }: { tokenProvider: () => Promise }): AuthTokenManager { + if (typeof tokenProvider !== 'function') { + throw new TypeError(`tokenProvider should be function, but got: ${typeof tokenProvider}`) } - return new TemporalAuthDataManager(getAuthData) + return new ExpirationBasedAuthTokenManager(tokenProvider) } /** @@ -129,7 +129,7 @@ export function isStaticAuthTokenManger (manager: AuthTokenManager): manager is } interface TokenRefreshObserver { - onCompleted: (data: TemporalAuthData) => void + onCompleted: (data: AuthTokenAndExpiration) => void onError: (error: Error) => void } @@ -142,7 +142,7 @@ class TokenRefreshObservable implements TokenRefreshObserver { this._subscribers.push(sub) } - onCompleted (data: TemporalAuthData): void { + onCompleted (data: AuthTokenAndExpiration): void { this._subscribers.forEach(sub => sub.onCompleted(data)) } @@ -151,10 +151,10 @@ class TokenRefreshObservable implements TokenRefreshObserver { } } -class TemporalAuthDataManager implements AuthTokenManager { +class ExpirationBasedAuthTokenManager implements AuthTokenManager { constructor ( - private readonly _getAuthData: () => Promise, - private _currentAuthData?: TemporalAuthData, + private readonly _tokenProvider: () => Promise, + private _currentAuthData?: AuthTokenAndExpiration, private _refreshObservable?: TokenRefreshObservable) { } @@ -162,8 +162,8 @@ class TemporalAuthDataManager implements AuthTokenManager { async getToken (): Promise { if (this._currentAuthData === undefined || ( - this._currentAuthData.expiry !== undefined && - this._currentAuthData.expiry < new Date() + this._currentAuthData.expiration !== undefined && + this._currentAuthData.expiration < new Date() )) { await this._refreshAuthToken() } @@ -182,7 +182,7 @@ class TemporalAuthDataManager implements AuthTokenManager { this._currentAuthData = undefined this._refreshObservable = new TokenRefreshObservable() - Promise.resolve(this._getAuthData()) + Promise.resolve(this._tokenProvider()) .then(data => { this._currentAuthData = data this._refreshObservable?.onCompleted(data) @@ -200,8 +200,8 @@ class TemporalAuthDataManager implements AuthTokenManager { } } - private async _refreshAuthToken (): Promise { - return await new Promise((resolve, reject) => { + private async _refreshAuthToken (): Promise { + return await new Promise((resolve, reject) => { this._scheduleRefreshAuthToken({ onCompleted: resolve, onError: reject diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 341b74039..c329b04e3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -87,7 +87,7 @@ import Session, { TransactionConfig } from './session' import Driver, * as driver from './driver' import auth from './auth' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager' -import AuthTokenManager, { temporalAuthDataManager, staticAuthTokenManager, isStaticAuthTokenManger, TemporalAuthData } from './auth-token-manager' +import AuthTokenManager, { expirationBasedAuthTokenManager, staticAuthTokenManager, isStaticAuthTokenManger, AuthTokenAndExpiration } from './auth-token-manager' import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver' import * as types from './types' import * as json from './json' @@ -164,7 +164,7 @@ const forExport = { json, auth, bookmarkManager, - temporalAuthDataManager, + expirationBasedAuthTokenManager, routing, resultTransformers, notificationCategory, @@ -232,7 +232,7 @@ export { json, auth, bookmarkManager, - temporalAuthDataManager, + expirationBasedAuthTokenManager, staticAuthTokenManager, isStaticAuthTokenManger, routing, @@ -253,7 +253,7 @@ export type { BookmarkManager, BookmarkManagerConfig, AuthTokenManager, - TemporalAuthData, + AuthTokenAndExpiration, SessionConfig, QueryConfig, RoutingControl, diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js index f04993bb6..9f8cc81bf 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/authentication-provider.js @@ -17,7 +17,7 @@ * limitations under the License. */ -import { temporalAuthDataManager } from '../../core/index.ts' +import { expirationBasedAuthTokenManager } from '../../core/index.ts' import { object } from '../lang/index.js' /** @@ -25,8 +25,8 @@ import { object } from '../lang/index.js' */ export default class AuthenticationProvider { constructor ({ authTokenManager, userAgent }) { - this._authTokenManager = authTokenManager || temporalAuthDataManager({ - getAuthData: () => {} + this._authTokenManager = authTokenManager || expirationBasedAuthTokenManager({ + tokenProvider: () => {} }) this._userAgent = userAgent } diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts index 70dd99811..f748100aa 100644 --- a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -52,14 +52,14 @@ export default class AuthTokenManager { } /** - * Interface which defines an {@link AuthToken} with an expiry data time associated + * Interface which defines an {@link AuthToken} with an expiration data time associated * @interface * @experimental Exposed as preview feature. * @since 5.7 */ -export class TemporalAuthData { +export class AuthTokenAndExpiration { public readonly token: AuthToken - public readonly expiry?: Date + public readonly expiration?: Date private constructor () { /** @@ -74,34 +74,34 @@ export class TemporalAuthData { * The expected expiration date of the auth token. * * This information will be used for triggering the auth token refresh - * in managers created with {@link temporalAuthDataManager}. + * in managers created with {@link expirationBasedAuthTokenManager}. * * If this value is not defined, the {@link AuthToken} will be considered valid * until a `Neo.ClientError.Security.TokenExpired` error happens. * * @type {Date|undefined} */ - this.expiry = undefined + this.expiration = undefined } } /** * Creates a {@link AuthTokenManager} for handle {@link AuthToken} which is expires. * - * **Warning**: `getAuthData` must only ever return auth information belonging to the same identity. + * **Warning**: `tokenProvider` must only ever return auth information belonging to the same identity. * Switching identities using the `AuthTokenManager` is undefined behavior. * * @param {object} param0 - The params - * @param {function(): Promise} param0.getAuthData - Retrieves a new valid auth token. + * @param {function(): Promise} param0.tokenProvider - Retrieves a new valid auth token. * Must only ever return auth information belonging to the same identity. * @returns {AuthTokenManager} The temporal auth data manager. * @experimental Exposed as preview feature. */ -export function temporalAuthDataManager ({ getAuthData }: { getAuthData: () => Promise }): AuthTokenManager { - if (typeof getAuthData !== 'function') { - throw new TypeError(`getAuthData should be function, but got: ${typeof getAuthData}`) +export function expirationBasedAuthTokenManager ({ tokenProvider }: { tokenProvider: () => Promise }): AuthTokenManager { + if (typeof tokenProvider !== 'function') { + throw new TypeError(`tokenProvider should be function, but got: ${typeof tokenProvider}`) } - return new TemporalAuthDataManager(getAuthData) + return new ExpirationBasedAuthTokenManager(tokenProvider) } /** @@ -129,7 +129,7 @@ export function isStaticAuthTokenManger (manager: AuthTokenManager): manager is } interface TokenRefreshObserver { - onCompleted: (data: TemporalAuthData) => void + onCompleted: (data: AuthTokenAndExpiration) => void onError: (error: Error) => void } @@ -142,7 +142,7 @@ class TokenRefreshObservable implements TokenRefreshObserver { this._subscribers.push(sub) } - onCompleted (data: TemporalAuthData): void { + onCompleted (data: AuthTokenAndExpiration): void { this._subscribers.forEach(sub => sub.onCompleted(data)) } @@ -151,10 +151,10 @@ class TokenRefreshObservable implements TokenRefreshObserver { } } -class TemporalAuthDataManager implements AuthTokenManager { +class ExpirationBasedAuthTokenManager implements AuthTokenManager { constructor ( - private readonly _getAuthData: () => Promise, - private _currentAuthData?: TemporalAuthData, + private readonly _tokenProvider: () => Promise, + private _currentAuthData?: AuthTokenAndExpiration, private _refreshObservable?: TokenRefreshObservable) { } @@ -162,8 +162,8 @@ class TemporalAuthDataManager implements AuthTokenManager { async getToken (): Promise { if (this._currentAuthData === undefined || ( - this._currentAuthData.expiry !== undefined && - this._currentAuthData.expiry < new Date() + this._currentAuthData.expiration !== undefined && + this._currentAuthData.expiration < new Date() )) { await this._refreshAuthToken() } @@ -182,7 +182,7 @@ class TemporalAuthDataManager implements AuthTokenManager { this._currentAuthData = undefined this._refreshObservable = new TokenRefreshObservable() - Promise.resolve(this._getAuthData()) + Promise.resolve(this._tokenProvider()) .then(data => { this._currentAuthData = data this._refreshObservable?.onCompleted(data) @@ -200,8 +200,8 @@ class TemporalAuthDataManager implements AuthTokenManager { } } - private async _refreshAuthToken (): Promise { - return await new Promise((resolve, reject) => { + private async _refreshAuthToken (): Promise { + return await new Promise((resolve, reject) => { this._scheduleRefreshAuthToken({ onCompleted: resolve, onError: reject diff --git a/packages/neo4j-driver-deno/lib/core/index.ts b/packages/neo4j-driver-deno/lib/core/index.ts index 1531b36c3..d9a1ab2f6 100644 --- a/packages/neo4j-driver-deno/lib/core/index.ts +++ b/packages/neo4j-driver-deno/lib/core/index.ts @@ -87,7 +87,7 @@ import Session, { TransactionConfig } from './session.ts' import Driver, * as driver from './driver.ts' import auth from './auth.ts' import BookmarkManager, { BookmarkManagerConfig, bookmarkManager } from './bookmark-manager.ts' -import AuthTokenManager, { temporalAuthDataManager, staticAuthTokenManager, isStaticAuthTokenManger, TemporalAuthData } from './auth-token-manager.ts' +import AuthTokenManager, { expirationBasedAuthTokenManager, staticAuthTokenManager, isStaticAuthTokenManger, AuthTokenAndExpiration } from './auth-token-manager.ts' import { SessionConfig, QueryConfig, RoutingControl, routing } from './driver.ts' import * as types from './types.ts' import * as json from './json.ts' @@ -164,7 +164,7 @@ const forExport = { json, auth, bookmarkManager, - temporalAuthDataManager, + expirationBasedAuthTokenManager, routing, resultTransformers, notificationCategory, @@ -232,7 +232,7 @@ export { json, auth, bookmarkManager, - temporalAuthDataManager, + expirationBasedAuthTokenManager, staticAuthTokenManager, isStaticAuthTokenManger, routing, @@ -253,7 +253,7 @@ export type { BookmarkManager, BookmarkManagerConfig, AuthTokenManager, - TemporalAuthData, + AuthTokenAndExpiration, SessionConfig, QueryConfig, RoutingControl, diff --git a/packages/neo4j-driver-deno/lib/mod.ts b/packages/neo4j-driver-deno/lib/mod.ts index 3433a25cb..2715877ce 100644 --- a/packages/neo4j-driver-deno/lib/mod.ts +++ b/packages/neo4j-driver-deno/lib/mod.ts @@ -95,8 +95,8 @@ import { notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, AuthTokenManager, - temporalAuthDataManager, - TemporalAuthData, + expirationBasedAuthTokenManager, + AuthTokenAndExpiration, staticAuthTokenManager } from './core/index.ts' // @deno-types=./bolt-connection/types/index.d.ts @@ -544,7 +544,7 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager + expirationBasedAuthTokenManager } export { @@ -611,13 +611,13 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager + expirationBasedAuthTokenManager } export type { QueryResult, AuthToken, AuthTokenManager, - TemporalAuthData, + AuthTokenAndExpiration, Config, EncryptionLevel, TrustStrategy, diff --git a/packages/neo4j-driver-lite/src/index.ts b/packages/neo4j-driver-lite/src/index.ts index 68a744a11..41fb4d73e 100644 --- a/packages/neo4j-driver-lite/src/index.ts +++ b/packages/neo4j-driver-lite/src/index.ts @@ -95,8 +95,8 @@ import { notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, AuthTokenManager, - temporalAuthDataManager, - TemporalAuthData, + expirationBasedAuthTokenManager, + AuthTokenAndExpiration, staticAuthTokenManager } from 'neo4j-driver-core' import { @@ -543,7 +543,7 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager + expirationBasedAuthTokenManager } export { @@ -610,13 +610,13 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager + expirationBasedAuthTokenManager } export type { QueryResult, AuthToken, AuthTokenManager, - TemporalAuthData, + AuthTokenAndExpiration, Config, EncryptionLevel, TrustStrategy, diff --git a/packages/neo4j-driver/src/index.js b/packages/neo4j-driver/src/index.js index f4dc7b7c3..012faaf97 100644 --- a/packages/neo4j-driver/src/index.js +++ b/packages/neo4j-driver/src/index.js @@ -74,7 +74,7 @@ import { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager, + expirationBasedAuthTokenManager, staticAuthTokenManager } from 'neo4j-driver-core' import { @@ -518,7 +518,7 @@ const forExport = { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager + expirationBasedAuthTokenManager } export { @@ -586,6 +586,6 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager + expirationBasedAuthTokenManager } export default forExport diff --git a/packages/neo4j-driver/types/index.d.ts b/packages/neo4j-driver/types/index.d.ts index 3963c481a..f6e3f7250 100644 --- a/packages/neo4j-driver/types/index.d.ts +++ b/packages/neo4j-driver/types/index.d.ts @@ -85,8 +85,8 @@ import { notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, AuthTokenManager, - TemporalAuthData, - temporalAuthDataManager + AuthTokenAndExpiration, + expirationBasedAuthTokenManager } from 'neo4j-driver-core' import { AuthToken, @@ -268,7 +268,7 @@ declare const forExport: { notificationSeverityLevel: typeof notificationSeverityLevel notificationFilterDisabledCategory: typeof notificationFilterDisabledCategory notificationFilterMinimumSeverityLevel: typeof notificationFilterMinimumSeverityLevel - temporalAuthDataManager: typeof temporalAuthDataManager + expirationBasedAuthTokenManager: typeof expirationBasedAuthTokenManager } export { @@ -343,7 +343,7 @@ export { notificationSeverityLevel, notificationFilterDisabledCategory, notificationFilterMinimumSeverityLevel, - temporalAuthDataManager + expirationBasedAuthTokenManager } export type { @@ -359,7 +359,7 @@ export type { NotificationFilterDisabledCategory, NotificationFilterMinimumSeverityLevel, AuthTokenManager, - TemporalAuthData + AuthTokenAndExpiration } export default forExport diff --git a/packages/testkit-backend/src/request-handlers-rx.js b/packages/testkit-backend/src/request-handlers-rx.js index bba152dd7..65a62b6af 100644 --- a/packages/testkit-backend/src/request-handlers-rx.js +++ b/packages/testkit-backend/src/request-handlers-rx.js @@ -26,11 +26,11 @@ export { StartSubTest, ExecuteQuery, NewAuthTokenManager, - NewTemporalAuthTokenManager, AuthTokenManagerClose, AuthTokenManagerGetAuthCompleted, AuthTokenManagerOnAuthExpiredCompleted, - TemporalAuthTokenProviderCompleted, + NewExpirationBasedAuthTokenManager, + ExpirationBasedAuthTokenProviderCompleted, FakeTimeInstall, FakeTimeTick, FakeTimeUninstall diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 3a69409eb..6c93ddf76 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -538,19 +538,6 @@ export function NewAuthTokenManager (_, context, _data, wire) { wire.writeResponse(responses.AuthTokenManager({ id })) } -export function NewTemporalAuthTokenManager ({ neo4j }, context, _, wire) { - const id = context.addAuthTokenManager((temporalAuthTokenManagerId) => { - return neo4j.temporalAuthDataManager({ - getAuthData: () => new Promise((resolve, reject) => { - const id = context.addTemporalAuthTokenProviderRequest(resolve, reject) - wire.writeResponse(responses.TemporalAuthTokenProviderRequest({ id, temporalAuthTokenManagerId })) - }) - }) - }) - - wire.writeResponse(responses.TemporalAuthTokenManager({ id })) -} - export function AuthTokenManagerClose (_, context, { id }, wire) { context.removeAuthTokenManager(id) wire.writeResponse(responses.AuthTokenManager({ id })) @@ -566,10 +553,23 @@ export function AuthTokenManagerOnAuthExpiredCompleted (_, context, { requestId context.removeAuthTokenManagerOnAuthExpiredRequest(requestId) } -export function TemporalAuthTokenProviderCompleted (_, context, { requestId, auth }) { +export function NewExpirationBasedAuthTokenManager ({ neo4j }, context, _, wire) { + const id = context.addAuthTokenManager((temporalAuthTokenManagerId) => { + return neo4j.expirationBasedAuthTokenManager({ + tokenProvider: () => new Promise((resolve, reject) => { + const id = context.addTemporalAuthTokenProviderRequest(resolve, reject) + wire.writeResponse(responses.TemporalAuthTokenProviderRequest({ id, temporalAuthTokenManagerId })) + }) + }) + }) + + wire.writeResponse(responses.TemporalAuthTokenManager({ id })) +} + +export function ExpirationBasedAuthTokenProviderCompleted (_, context, { requestId, auth }) { const request = context.getTemporalAuthTokenProviderRequest(requestId) request.resolve({ - expiry: auth.data.expiresInMs != null + expiration: auth.data.expiresInMs != null ? new Date(new Date().getTime() + auth.data.expiresInMs) : undefined, token: context.binder.parseAuthToken(auth.data.auth.data) diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index 101a5cc19..bf6c35783 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -120,11 +120,11 @@ export function AuthTokenManagerOnAuthExpiredRequest ({ id, authTokenManagerId, } export function TemporalAuthTokenManager ({ id }) { - return response('TemporalAuthTokenManager', { id }) + return response('ExpirationBasedAuthTokenManager', { id }) } export function TemporalAuthTokenProviderRequest ({ id, temporalAuthTokenManagerId }) { - return response('TemporalAuthTokenProviderRequest', { id, temporalAuthTokenManagerId }) + return response('ExpirationBasedAuthTokenProviderRequest', { id, expirationBasedAuthTokenManagerId: temporalAuthTokenManagerId }) } export function DriverIsAuthenticated ({ id, authenticated }) { From 0a02a2a0c16f646d034c1bfcb2e4f541d4d3b132 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 4 Apr 2023 11:29:20 +0200 Subject: [PATCH 63/70] Fix names in testkit-backend --- packages/testkit-backend/src/context.js | 14 +++++++------- packages/testkit-backend/src/request-handlers.js | 12 ++++++------ packages/testkit-backend/src/responses.js | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/testkit-backend/src/context.js b/packages/testkit-backend/src/context.js index 0754a921d..f38520c76 100644 --- a/packages/testkit-backend/src/context.js +++ b/packages/testkit-backend/src/context.js @@ -15,7 +15,7 @@ export default class Context { this._authTokenManagers = {} this._authTokenManagerGetAuthRequests = {} this._authTokenManagerOnAuthExpiredRequests = {} - this._temporalAuthTokenProviderRequests = {} + this._expirationBasedAuthTokenProviderRequests = {} this._binder = binder this._environmentLogLevel = environmentLogLevel } @@ -201,16 +201,16 @@ export default class Context { delete this._authTokenManagerOnAuthExpiredRequests[id] } - addTemporalAuthTokenProviderRequest (resolve, reject) { - return this._add(this._temporalAuthTokenProviderRequests, { resolve, reject }) + addExpirationBasedAuthTokenProviderRequest (resolve, reject) { + return this._add(this._expirationBasedAuthTokenProviderRequests, { resolve, reject }) } - getTemporalAuthTokenProviderRequest (id) { - return this._temporalAuthTokenProviderRequests[id] + getExpirationBasedAuthTokenProviderRequest (id) { + return this._expirationBasedAuthTokenProviderRequests[id] } - removeTemporalAuthTokenProviderRequest (id) { - delete this._temporalAuthTokenProviderRequests[id] + removeExpirationBasedAuthTokenProviderRequest (id) { + delete this._expirationBasedAuthTokenProviderRequests[id] } _add (map, object) { diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 6c93ddf76..0866fe7be 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -554,27 +554,27 @@ export function AuthTokenManagerOnAuthExpiredCompleted (_, context, { requestId } export function NewExpirationBasedAuthTokenManager ({ neo4j }, context, _, wire) { - const id = context.addAuthTokenManager((temporalAuthTokenManagerId) => { + const id = context.addAuthTokenManager((expirationBasedAuthTokenManagerId) => { return neo4j.expirationBasedAuthTokenManager({ tokenProvider: () => new Promise((resolve, reject) => { - const id = context.addTemporalAuthTokenProviderRequest(resolve, reject) - wire.writeResponse(responses.TemporalAuthTokenProviderRequest({ id, temporalAuthTokenManagerId })) + const id = context.addExpirationBasedAuthTokenProviderRequest(resolve, reject) + wire.writeResponse(responses.ExpirationBasedAuthTokenProviderRequest({ id, expirationBasedAuthTokenManagerId })) }) }) }) - wire.writeResponse(responses.TemporalAuthTokenManager({ id })) + wire.writeResponse(responses.ExpirationBasedAuthTokenManager({ id })) } export function ExpirationBasedAuthTokenProviderCompleted (_, context, { requestId, auth }) { - const request = context.getTemporalAuthTokenProviderRequest(requestId) + const request = context.getExpirationBasedAuthTokenProviderRequest(requestId) request.resolve({ expiration: auth.data.expiresInMs != null ? new Date(new Date().getTime() + auth.data.expiresInMs) : undefined, token: context.binder.parseAuthToken(auth.data.auth.data) }) - context.removeTemporalAuthTokenProviderRequest(requestId) + context.removeExpirationBasedAuthTokenProviderRequest(requestId) } export function GetRoutingTable (_, context, { driverId, database }, wire) { diff --git a/packages/testkit-backend/src/responses.js b/packages/testkit-backend/src/responses.js index bf6c35783..9d223f0e2 100644 --- a/packages/testkit-backend/src/responses.js +++ b/packages/testkit-backend/src/responses.js @@ -119,12 +119,12 @@ export function AuthTokenManagerOnAuthExpiredRequest ({ id, authTokenManagerId, return response('AuthTokenManagerOnAuthExpiredRequest', { id, authTokenManagerId, auth: AuthorizationToken(auth) }) } -export function TemporalAuthTokenManager ({ id }) { +export function ExpirationBasedAuthTokenManager ({ id }) { return response('ExpirationBasedAuthTokenManager', { id }) } -export function TemporalAuthTokenProviderRequest ({ id, temporalAuthTokenManagerId }) { - return response('ExpirationBasedAuthTokenProviderRequest', { id, expirationBasedAuthTokenManagerId: temporalAuthTokenManagerId }) +export function ExpirationBasedAuthTokenProviderRequest ({ id, expirationBasedAuthTokenManagerId }) { + return response('ExpirationBasedAuthTokenProviderRequest', { id, expirationBasedAuthTokenManagerId }) } export function DriverIsAuthenticated ({ id, authenticated }) { From 60540699df3833ec0d4e4434682831ad93cae336 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Tue, 4 Apr 2023 12:28:59 +0200 Subject: [PATCH 64/70] Ajust docs --- packages/core/src/auth-token-manager.ts | 4 ++-- packages/core/src/driver.ts | 4 ++-- packages/neo4j-driver-deno/lib/core/auth-token-manager.ts | 4 ++-- packages/neo4j-driver-deno/lib/core/driver.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/auth-token-manager.ts b/packages/core/src/auth-token-manager.ts index 7e1d1c42c..77a2961cc 100644 --- a/packages/core/src/auth-token-manager.ts +++ b/packages/core/src/auth-token-manager.ts @@ -25,7 +25,7 @@ import { util } from './internal' * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. * @interface * @experimental Exposed as preview feature. - * @since 5.7 + * @since 5.8 */ export default class AuthTokenManager { /** @@ -55,7 +55,7 @@ export default class AuthTokenManager { * Interface which defines an {@link AuthToken} with an expiration data time associated * @interface * @experimental Exposed as preview feature. - * @since 5.7 + * @since 5.8 */ export class AuthTokenAndExpiration { public readonly token: AuthToken diff --git a/packages/core/src/driver.ts b/packages/core/src/driver.ts index 1d7e47d40..1232c77eb 100644 --- a/packages/core/src/driver.ts +++ b/packages/core/src/driver.ts @@ -202,7 +202,7 @@ class SessionConfig { * driver creation. This configuration allows switch user and/or authorization information for the * session lifetime. * - * **Warning**: This option is only enable by default when the driver is connected with Neo4j Database servers + * **Warning**: This option is only enable when the driver is connected with Neo4j Database servers * which supports Bolt 5.1 and onwards. * * @type {AuthToken|undefined} @@ -810,7 +810,7 @@ class Driver { impersonatedUser?: string fetchSize: number bookmarkManager?: BookmarkManager - notificationFilter?: NotificationFilter, + notificationFilter?: NotificationFilter auth?: AuthToken }): Session { const sessionMode = Session._validateSessionMode(defaultAccessMode) diff --git a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts index f748100aa..c692be877 100644 --- a/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts +++ b/packages/neo4j-driver-deno/lib/core/auth-token-manager.ts @@ -25,7 +25,7 @@ import { util } from './internal/index.ts' * Interface for the piece of software responsible for keeping track of current active {@link AuthToken} across the driver. * @interface * @experimental Exposed as preview feature. - * @since 5.7 + * @since 5.8 */ export default class AuthTokenManager { /** @@ -55,7 +55,7 @@ export default class AuthTokenManager { * Interface which defines an {@link AuthToken} with an expiration data time associated * @interface * @experimental Exposed as preview feature. - * @since 5.7 + * @since 5.8 */ export class AuthTokenAndExpiration { public readonly token: AuthToken diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 23881fced..08d33f086 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -202,7 +202,7 @@ class SessionConfig { * driver creation. This configuration allows switch user and/or authorization information for the * session lifetime. * - * **Warning**: This option is only enable by default when the driver is connected with Neo4j Database servers + * **Warning**: This option is only enable when the driver is connected with Neo4j Database servers * which supports Bolt 5.1 and onwards. * * @type {AuthToken|undefined} From 7e6b9b2d285c7e20e0a0e953a59f6e3ea91c6192 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 5 Apr 2023 11:25:46 +0200 Subject: [PATCH 65/70] testkit-backend: Fix `authToken` param in VerifyAuthentication --- packages/testkit-backend/src/request-handlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index 0866fe7be..cf4cea78a 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -396,7 +396,7 @@ export function VerifyConnectivity (_, context, { driverId }, wire) { .catch(error => wire.writeError(error)) } -export function VerifyAuthentication (_, context, { driverId, auth_token: authToken }, wire) { +export function VerifyAuthentication (_, context, { driverId, authToken }, wire) { const auth = authToken != null && authToken.data != null ? context.binder.parseAuthToken(authToken.data) : undefined From 5df0cefd84de0300e234a6d792185c4f14476f8f Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 5 Apr 2023 11:27:31 +0200 Subject: [PATCH 66/70] Remove file added by acident --- .../home-database-provider.js | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/home-database-provider.js diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/home-database-provider.js b/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/home-database-provider.js deleted file mode 100644 index 9f28d1fbf..000000000 --- a/packages/neo4j-driver-deno/lib/bolt-connection/connection-provider/home-database-provider.js +++ /dev/null @@ -1,32 +0,0 @@ - - - -export class HomeDatabaseProvider { - constructor(ttlInSeconds = -1, cache = new Map()) { - this._ttlMs = ttlInSeconds * 1000 - this._cache = cache - } - - getDatabaseName ({ database, auth, impersonatedUser }) { - return database - if (database != null && database !== '') { - return database - } - - const key = impersonatedUser || auth || null - - if (this._cache.has(key)) { - const { createdAt, database: resolvedDatabase } = this._cache.get(key) - if ((Date.now() - createdAt) < this._ttlMs) { - return resolvedDatabase - } - } - - return database - } - - setDatabaseName ({ database, auth, impersonatedUser }) { - const key = impersonatedUser || auth || null - this._cache.set(key, { createdAt: Date.now(), database }) - } -} From 346b334ada418179c6cd5184ffc375c05e8c7c99 Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 5 Apr 2023 14:11:45 +0200 Subject: [PATCH 67/70] testkit-backend: Adjust authToken name in VerifyAuthentication --- packages/testkit-backend/src/request-handlers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/testkit-backend/src/request-handlers.js b/packages/testkit-backend/src/request-handlers.js index cf4cea78a..468a3b8ef 100644 --- a/packages/testkit-backend/src/request-handlers.js +++ b/packages/testkit-backend/src/request-handlers.js @@ -396,9 +396,9 @@ export function VerifyConnectivity (_, context, { driverId }, wire) { .catch(error => wire.writeError(error)) } -export function VerifyAuthentication (_, context, { driverId, authToken }, wire) { - const auth = authToken != null && authToken.data != null - ? context.binder.parseAuthToken(authToken.data) +export function VerifyAuthentication (_, context, { driverId, authorizationToken }, wire) { + const auth = authorizationToken != null && authorizationToken.data != null + ? context.binder.parseAuthToken(authorizationToken.data) : undefined const driver = context.getDriver(driverId) From 7548e5a49fd26e6fc322848eaf519f5b48cd34ed Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 5 Apr 2023 21:36:43 +0200 Subject: [PATCH 68/70] Sync DenoJS --- packages/neo4j-driver-deno/lib/core/driver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neo4j-driver-deno/lib/core/driver.ts b/packages/neo4j-driver-deno/lib/core/driver.ts index 08d33f086..4824acc91 100644 --- a/packages/neo4j-driver-deno/lib/core/driver.ts +++ b/packages/neo4j-driver-deno/lib/core/driver.ts @@ -810,7 +810,7 @@ class Driver { impersonatedUser?: string fetchSize: number bookmarkManager?: BookmarkManager - notificationFilter?: NotificationFilter, + notificationFilter?: NotificationFilter auth?: AuthToken }): Session { const sessionMode = Session._validateSessionMode(defaultAccessMode) From 73069800a92f23d1f07da76d7ec0d63138b3082b Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 5 Apr 2023 22:21:41 +0200 Subject: [PATCH 69/70] PolyfillFlatMap --- .../authentication-provider.test.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/bolt-connection/test/connection-provider/authentication-provider.test.js b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js index f9da1d3f4..97d4dccc4 100644 --- a/packages/bolt-connection/test/connection-provider/authentication-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js @@ -685,11 +685,11 @@ describe('AuthenticationProvider', () => { ] function nonValidCodesScenarios () { - return [ + return polyfillFlatMap([ 'Neo.ClientError.Security.AuthorizationExpired', 'Neo.ClientError.General.ForbiddenOnReadOnlyDatabase', 'Neo.Made.Up.Error' - ].flatMap(code => [ + ]).flatMap(code => [ [ `connection and provider has same auth token and error code does not trigger re-fresh (code=${code})`, () => { const authToken = { scheme: 'bearer', credentials: 'token' } @@ -733,11 +733,11 @@ describe('AuthenticationProvider', () => { } function nonValidCodesWithDifferentAuthScenarios () { - return [ + return polyfillFlatMap([ 'Neo.ClientError.Security.AuthorizationExpired', 'Neo.ClientError.General.ForbiddenOnReadOnlyDatabase', 'Neo.Made.Up.Error' - ].flatMap(code => [ + ]).flatMap(code => [ [ `connection and provider has different auth token and error code does not trigger re-fresh (code=${code})`, () => { @@ -825,6 +825,16 @@ describe('AuthenticationProvider', () => { ] } + function polyfillFlatMap (arr) { + /** Polyfill flatMap for Node10 tests */ + if (!arr.flatMap) { + arr.flatMap = function (callback, thisArg) { + return arr.concat.apply([], arr.map(callback, thisArg)) + } + } + return arr + } + function refreshObserverMock () { const subscribers = [] From 731dbdd86f35ae826e3ceb5af0aac2ff7032b2ef Mon Sep 17 00:00:00 2001 From: Antonio Barcelos Date: Wed, 5 Apr 2023 22:42:18 +0200 Subject: [PATCH 70/70] polyfill flatmap --- .../test/connection-provider/authentication-provider.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bolt-connection/test/connection-provider/authentication-provider.test.js b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js index 97d4dccc4..99dc57600 100644 --- a/packages/bolt-connection/test/connection-provider/authentication-provider.test.js +++ b/packages/bolt-connection/test/connection-provider/authentication-provider.test.js @@ -819,10 +819,10 @@ describe('AuthenticationProvider', () => { } function errorCodeTriggerRefreshAuth () { - return [ + return polyfillFlatMap([ 'Neo.ClientError.Security.Unauthorized', 'Neo.ClientError.Security.TokenExpired' - ] + ]) } function polyfillFlatMap (arr) {