Skip to content

Commit 5a33a58

Browse files
committed
Propagate authentication errors
Authentication errors happen during connection initialization when INIT message is send. Server validates provided credentials and then responds with either SUCCESS if credentials are fine or with FAILURE and closes the connection when credentials are wrong. Previously auth errors were not propagated to external observers (including those defined by user's Result promise). This means auth errors were only propagated to `Driver.onError` callback but all other places received `ServiceUnavailable` or `SessionExpired` because of closed connection. This commit makes driver treat INIT error as fatal and corresponding connection as broken. Initialization error is memorized and propagated to all message observers. So they see specific failure reason and not generic `ServiceUnavailable` \ `SessionExpired`.
1 parent 37e6382 commit 5a33a58

File tree

6 files changed

+130
-5
lines changed

6 files changed

+130
-5
lines changed

src/v1/internal/connector.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,20 @@ class Connection {
288288
}
289289
}
290290

291+
/**
292+
* Mark this connection as failed because processing of INIT message failed and server will close the connection.
293+
* Initialization failure is a fatal error for the connection.
294+
* @param {Neo4jError} error the initialization error.
295+
* @param {StreamObserver} initObserver the initialization observer that noticed the failure.
296+
* @protected
297+
*/
298+
_initializationFailed(error, initObserver) {
299+
if (this._currentObserver === initObserver) {
300+
this._currentObserver = null; // init observer detected the failure and should not be notified again
301+
}
302+
this._handleFatalError(error);
303+
}
304+
291305
_handleMessage( msg ) {
292306
const payload = msg.fields[0];
293307

@@ -351,7 +365,8 @@ class Connection {
351365
/** Queue an INIT-message to be sent to the database */
352366
initialize( clientName, token, observer ) {
353367
log("C", "INIT", clientName, token);
354-
this._queueObserver(observer);
368+
const initObserver = new InitObserver(this, observer);
369+
this._queueObserver(initObserver);
355370
this._packer.packStruct( INIT, [this._packable(clientName), this._packable(token)],
356371
(err) => this._handleFatalError(err) );
357372
this._chunker.messageBoundary();
@@ -489,6 +504,40 @@ function connect(url, config = {}, connectionErrorCode = null) {
489504
return new Connection( new Ch(channelConfig), completeUrl);
490505
}
491506

507+
/**
508+
* Observer that wraps user-defined observer for INIT message and handles initialization failures. Connection is
509+
* closed by the server if processing of INIT message fails so this observer will handle initialization failure
510+
* as a fatal error.
511+
*/
512+
class InitObserver {
513+
514+
/**
515+
* @constructor
516+
* @param {Connection} connection the connection used to send INIT message.
517+
* @param {StreamObserver} originalObserver the observer to wrap and delegate calls to.
518+
*/
519+
constructor(connection, originalObserver) {
520+
this._connection = connection;
521+
this._originalObserver = originalObserver || NO_OP_OBSERVER;
522+
}
523+
524+
onNext(record) {
525+
this._originalObserver.onNext(record);
526+
}
527+
528+
onError(error) {
529+
try {
530+
this._originalObserver.onError(error);
531+
} finally {
532+
this._connection._initializationFailed(error, this);
533+
}
534+
}
535+
536+
onCompleted(metaData) {
537+
this._originalObserver.onCompleted(metaData);
538+
}
539+
}
540+
492541
export {
493542
connect,
494543
parseScheme,

src/v1/internal/get-servers-util.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Integer, {int} from '../integer';
2323

2424
const PROCEDURE_CALL = 'CALL dbms.cluster.routing.getServers';
2525
const PROCEDURE_NOT_FOUND_CODE = 'Neo.ClientError.Procedure.ProcedureNotFound';
26+
const UNAUTHORIZED_CODE = 'Neo.ClientError.Security.Unauthorized';
2627

2728
export default class GetServersUtil {
2829

@@ -35,10 +36,14 @@ export default class GetServersUtil {
3536
// throw when getServers procedure not found because this is clearly a configuration issue
3637
throw newError('Server ' + routerAddress + ' could not perform routing. ' +
3738
'Make sure you are connecting to a causal cluster', SERVICE_UNAVAILABLE);
39+
} else if (error.code === UNAUTHORIZED_CODE) {
40+
// auth error is a sign of a configuration issue, rediscovery should not proceed
41+
throw error;
42+
} else {
43+
// return nothing when failed to connect because code higher in the callstack is still able to retry with a
44+
// different session towards a different router
45+
return null;
3846
}
39-
// return nothing when failed to connect because code higher in the callstack is still able to retry with a
40-
// different session towards a different router
41-
return null;
4247
});
4348
}
4449

test/internal/connector.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,30 @@ describe('connector', () => {
117117
channel.onmessage(packedFailureMessage(errorCode, errorMessage));
118118
});
119119

120+
it('should notify provided observer when connection initialization completes', done => {
121+
const connection = connect('bolt://localhost');
122+
123+
connection.initialize('mydriver/0.0.0', basicAuthToken(), {
124+
onCompleted: metaData => {
125+
expect(connection.isOpen()).toBeTruthy();
126+
expect(metaData).toBeDefined();
127+
done();
128+
},
129+
});
130+
});
131+
132+
it('should notify provided observer when connection initialization fails', done => {
133+
const connection = connect('bolt://localhost:7474'); // wrong port
134+
135+
connection.initialize('mydriver/0.0.0', basicAuthToken(), {
136+
onError: error => {
137+
expect(connection.isOpen()).toBeFalsy();
138+
expect(error).toBeDefined();
139+
done();
140+
},
141+
});
142+
});
143+
120144
function packedHandshakeMessage() {
121145
const result = alloc(4);
122146
result.putInt32(0, 1);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
!: AUTO RESET
2+
!: AUTO PULL_ALL
3+
4+
C: INIT "neo4j-javascript/0.0.0-dev" {"credentials": "neo4j", "scheme": "basic", "principal": "neo4j"}
5+
S: FAILURE {"code": "Neo.ClientError.Security.Unauthorized", "message": "Some server auth error message"}
6+
S: <EXIT>

test/v1/driver.test.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('driver', () => {
8080

8181
it('should fail early on wrong credentials', done => {
8282
// Given
83-
driver = neo4j.driver("bolt://localhost", neo4j.auth.basic("neo4j", "who would use such a password"));
83+
driver = neo4j.driver("bolt://localhost", wrongCredentials());
8484

8585
// Expect
8686
driver.onError = err => {
@@ -93,6 +93,16 @@ describe('driver', () => {
9393
startNewTransaction(driver);
9494
});
9595

96+
it('should fail queries on wrong credentials', done => {
97+
driver = neo4j.driver("bolt://localhost", wrongCredentials());
98+
99+
const session = driver.session();
100+
session.run('RETURN 1').catch(error => {
101+
expect(error.code).toEqual('Neo.ClientError.Security.Unauthorized');
102+
done();
103+
});
104+
});
105+
96106
it('should indicate success early on correct credentials', done => {
97107
// Given
98108
driver = neo4j.driver("bolt://localhost", sharedNeo4j.authToken);
@@ -207,4 +217,8 @@ describe('driver', () => {
207217
expect(session.beginTransaction()).toBeDefined();
208218
}
209219

220+
function wrongCredentials() {
221+
return neo4j.auth.basic('neo4j', 'who would use such a password');
222+
}
223+
210224
});

test/v1/routing.driver.boltkit.it.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,6 +1549,33 @@ describe('routing driver', () => {
15491549
});
15501550
});
15511551

1552+
it('should fail rediscovery on auth error', done => {
1553+
if (!boltkit.BoltKitSupport) {
1554+
done();
1555+
return;
1556+
}
1557+
1558+
const kit = new boltkit.BoltKit();
1559+
const router = kit.start('./test/resources/boltkit/failed_auth.script', 9010);
1560+
1561+
kit.run(() => {
1562+
const driver = newDriver('bolt+routing://127.0.0.1:9010');
1563+
const session = driver.session();
1564+
session.run('RETURN 1').catch(error => {
1565+
expect(error.code).toEqual('Neo.ClientError.Security.Unauthorized');
1566+
expect(error.message).toEqual('Some server auth error message');
1567+
1568+
session.close(() => {
1569+
driver.close();
1570+
router.exit(code => {
1571+
expect(code).toEqual(0);
1572+
done();
1573+
});
1574+
});
1575+
});
1576+
});
1577+
});
1578+
15521579
function moveNextDateNow30SecondsForward() {
15531580
const currentTime = Date.now();
15541581
hijackNextDateNowCall(currentTime + 30 * 1000 + 1);

0 commit comments

Comments
 (0)