Skip to content

Commit 5d3b349

Browse files
authored
Merge pull request #239 from lutovich/1.2-auth-error
Propagate authentication errors
2 parents bef4127 + 6294e36 commit 5d3b349

File tree

6 files changed

+161
-9
lines changed

6 files changed

+161
-9
lines changed

src/v1/internal/connector.js

+57-5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {alloc} from './buf';
2424
import {Node, Path, PathSegment, Relationship, UnboundRelationship} from '../graph-types';
2525
import {newError} from './../error';
2626
import ChannelConfig from './ch-config';
27+
import StreamObserver from './stream-observer';
2728

2829
let Channel;
2930
if( NodeChannel.available ) {
@@ -272,7 +273,7 @@ class Connection {
272273
* failing, and the connection getting ejected from the session pool.
273274
*
274275
* @param err an error object, forwarded to all current and future subscribers
275-
* @private
276+
* @protected
276277
*/
277278
_handleFatalError( err ) {
278279
this._isBroken = true;
@@ -289,6 +290,12 @@ class Connection {
289290
}
290291

291292
_handleMessage( msg ) {
293+
if (this._isBroken) {
294+
// ignore all incoming messages when this connection is broken. all previously pending observers failed
295+
// with the fatal error. all future observers will fail with same fatal error.
296+
return;
297+
}
298+
292299
const payload = msg.fields[0];
293300

294301
switch( msg.signature ) {
@@ -301,7 +308,7 @@ class Connection {
301308
try {
302309
this._currentObserver.onCompleted( payload );
303310
} finally {
304-
this._currentObserver = this._pendingObservers.shift();
311+
this._updateCurrentObserver();
305312
}
306313
break;
307314
case FAILURE:
@@ -310,7 +317,7 @@ class Connection {
310317
this._currentFailure = newError(payload.message, payload.code);
311318
this._currentObserver.onError( this._currentFailure );
312319
} finally {
313-
this._currentObserver = this._pendingObservers.shift();
320+
this._updateCurrentObserver();
314321
// Things are now broken. Pending observers will get FAILURE messages routed until
315322
// We are done handling this failure.
316323
if( !this._isHandlingFailure ) {
@@ -340,7 +347,7 @@ class Connection {
340347
else if(this._currentObserver.onError)
341348
this._currentObserver.onError(payload);
342349
} finally {
343-
this._currentObserver = this._pendingObservers.shift();
350+
this._updateCurrentObserver();
344351
}
345352
break;
346353
default:
@@ -351,7 +358,8 @@ class Connection {
351358
/** Queue an INIT-message to be sent to the database */
352359
initialize( clientName, token, observer ) {
353360
log("C", "INIT", clientName, token);
354-
this._queueObserver(observer);
361+
const initObserver = new InitObserver(this, observer);
362+
this._queueObserver(initObserver);
355363
this._packer.packStruct( INIT, [this._packable(clientName), this._packable(token)],
356364
(err) => this._handleFatalError(err) );
357365
this._chunker.messageBoundary();
@@ -437,6 +445,14 @@ class Connection {
437445
}
438446
}
439447

448+
/**
449+
* Pop next pending observer form the list of observers and make it current observer.
450+
* @protected
451+
*/
452+
_updateCurrentObserver() {
453+
this._currentObserver = this._pendingObservers.shift();
454+
}
455+
440456
/**
441457
* Synchronize - flush all queued outgoing messages and route their responses
442458
* to their respective handlers.
@@ -489,6 +505,42 @@ function connect(url, config = {}, connectionErrorCode = null) {
489505
return new Connection( new Ch(channelConfig), completeUrl);
490506
}
491507

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

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

+8-3
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

+48
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,54 @@ 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+
144+
it('should fail all new observers after initialization error', done => {
145+
const connection = connect('bolt://localhost:7474'); // wrong port
146+
147+
connection.initialize('mydriver/0.0.0', basicAuthToken(), {
148+
onError: initialError => {
149+
expect(initialError).toBeDefined();
150+
151+
connection.run('RETURN 1', {}, {
152+
onError: error1 => {
153+
expect(error1).toEqual(initialError);
154+
155+
connection.initialize('mydriver/0.0.0', basicAuthToken(), {
156+
onError: error2 => {
157+
expect(error2).toEqual(initialError);
158+
159+
done();
160+
}
161+
});
162+
}
163+
});
164+
},
165+
});
166+
});
167+
120168
function packedHandshakeMessage() {
121169
const result = alloc(4);
122170
result.putInt32(0, 1);
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

+15-1
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

+27
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)