Skip to content

Commit ce4d679

Browse files
authored
Introducing Bearer tokens and SSO support (#783)
This change provides a utility function for easy create auth token object for `bearer` tokens. The error handling related with this kind of token usage is also implemented. When a `Neo.ClientError.Security.TokenExpired` error happens, the address should be purged from the pool and the caller should be notified - no retry should be performed.
1 parent 9df4152 commit ce4d679

File tree

16 files changed

+464
-107
lines changed

16 files changed

+464
-107
lines changed

packages/bolt-connection/src/bolt/bolt-protocol-v1.js

+4
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,10 @@ export default class BoltProtocol {
296296
return observer
297297
}
298298

299+
get currentFailure () {
300+
return this._responseHandler.currentFailure
301+
}
302+
299303
/**
300304
* Send a RESET through the underlying connection.
301305
* @param {Object} param

packages/bolt-connection/src/bolt/response-handler.js

+5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export default class ResponseHandler {
8686
)
8787
}
8888

89+
get currentFailure () {
90+
return this._currentFailure
91+
}
92+
8993
handleResponse (msg) {
9094
const payload = msg.fields[0]
9195

@@ -186,4 +190,5 @@ export default class ResponseHandler {
186190
_resetFailure () {
187191
this._currentFailure = null
188192
}
193+
189194
}

packages/bolt-connection/src/connection/connection-channel.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ export default class ChannelConnection extends Connection {
270270
*/
271271
_handleFatalError (error) {
272272
this._isBroken = true
273-
this._error = this.handleAndTransformError(error, this._address)
273+
this._error = this.handleAndTransformError(this._protocol.currentFailure || error, this._address)
274274

275275
if (this._log.isErrorEnabled()) {
276276
this._log.error(

packages/bolt-connection/src/connection/connection-error-handler.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ export default class ConnectionErrorHandler {
7777
}
7878

7979
function isAutorizationExpiredError (error) {
80-
return error && error.code === 'Neo.ClientError.Security.AuthorizationExpired'
80+
return error && (
81+
error.code === 'Neo.ClientError.Security.AuthorizationExpired' ||
82+
error.code === 'Neo.ClientError.Security.TokenExpired'
83+
)
8184
}
8285

8386
function isAvailabilityError (error) {

packages/bolt-connection/test/connection-provider/connection-provider-direct.test.js

+41
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,47 @@ describe('#unit DirectConnectionProvider', () => {
9898
})
9999
})
100100

101+
it('should purge connections for address when TokenExpired happens', async () => {
102+
const address = ServerAddress.fromUrl('localhost:123')
103+
const pool = newPool()
104+
jest.spyOn(pool, 'purge')
105+
const connectionProvider = newDirectConnectionProvider(address, pool)
106+
107+
const conn = await connectionProvider.acquireConnection({
108+
accessMode: 'READ',
109+
database: ''
110+
})
111+
112+
const error = newError(
113+
'Message',
114+
'Neo.ClientError.Security.TokenExpired'
115+
)
116+
117+
conn.handleAndTransformError(error, address)
118+
119+
expect(pool.purge).toHaveBeenCalledWith(address)
120+
})
121+
122+
it('should not change error when TokenExpired happens', async () => {
123+
const address = ServerAddress.fromUrl('localhost:123')
124+
const pool = newPool()
125+
const connectionProvider = newDirectConnectionProvider(address, pool)
126+
127+
const conn = await connectionProvider.acquireConnection({
128+
accessMode: 'READ',
129+
database: ''
130+
})
131+
132+
const expectedError = newError(
133+
'Message',
134+
'Neo.ClientError.Security.TokenExpired'
135+
)
136+
137+
const error = conn.handleAndTransformError(expectedError, address)
138+
139+
expect(error).toBe(expectedError)
140+
})
141+
101142
function newDirectConnectionProvider (address, pool) {
102143
const connectionProvider = new DirectConnectionProvider({
103144
id: 0,

packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js

+148
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,80 @@ describe('#unit RoutingConnectionProvider', () => {
14931493
expect(error).toBe(expectedError)
14941494
})
14951495

1496+
it('should purge connections for address when TokenExpired happens', async () => {
1497+
const pool = newPool()
1498+
1499+
jest.spyOn(pool, 'purge')
1500+
1501+
const connectionProvider = newRoutingConnectionProvider(
1502+
[
1503+
newRoutingTable(
1504+
null,
1505+
[server1, server2],
1506+
[server3, server2],
1507+
[server2, server4]
1508+
)
1509+
],
1510+
pool
1511+
)
1512+
1513+
const error = newError(
1514+
'Message',
1515+
'Neo.ClientError.Security.TokenExpired'
1516+
)
1517+
1518+
const server2Connection = await connectionProvider.acquireConnection({
1519+
accessMode: 'WRITE',
1520+
database: null
1521+
})
1522+
1523+
const server3Connection = await connectionProvider.acquireConnection({
1524+
accessMode: 'READ',
1525+
database: null
1526+
})
1527+
1528+
server3Connection.handleAndTransformError(error, server3)
1529+
server2Connection.handleAndTransformError(error, server2)
1530+
1531+
expect(pool.purge).toHaveBeenCalledWith(server3)
1532+
expect(pool.purge).toHaveBeenCalledWith(server2)
1533+
})
1534+
1535+
it('should not change error when TokenExpired happens', async () => {
1536+
const pool = newPool()
1537+
1538+
jest.spyOn(pool, 'purge')
1539+
1540+
const connectionProvider = newRoutingConnectionProvider(
1541+
[
1542+
newRoutingTable(
1543+
null,
1544+
[server1, server2],
1545+
[server3, server2],
1546+
[server2, server4]
1547+
)
1548+
],
1549+
pool
1550+
)
1551+
1552+
const expectedError = newError(
1553+
'Message',
1554+
'Neo.ClientError.Security.TokenExpired'
1555+
)
1556+
1557+
const server2Connection = await connectionProvider.acquireConnection({
1558+
accessMode: 'WRITE',
1559+
database: null
1560+
})
1561+
1562+
const error = server2Connection.handleAndTransformError(
1563+
expectedError,
1564+
server2
1565+
)
1566+
1567+
expect(error).toBe(expectedError)
1568+
})
1569+
14961570
it('should use resolved seed router after accepting table with no writers', done => {
14971571
const routingTable1 = newRoutingTable(
14981572
null,
@@ -1674,6 +1748,80 @@ describe('#unit RoutingConnectionProvider', () => {
16741748
expect(error).toBe(expectedError)
16751749
})
16761750

1751+
it('should purge connections for address when TokenExpired happens', async () => {
1752+
const pool = newPool()
1753+
1754+
jest.spyOn(pool, 'purge')
1755+
1756+
const connectionProvider = newRoutingConnectionProvider(
1757+
[
1758+
newRoutingTable(
1759+
'databaseA',
1760+
[server1, server2],
1761+
[server1],
1762+
[server2]
1763+
),
1764+
newRoutingTable('databaseB', [serverA, serverB], [serverA], [serverB])
1765+
],
1766+
pool
1767+
)
1768+
1769+
const error = newError(
1770+
'Message',
1771+
'Neo.ClientError.Security.TokenExpired'
1772+
)
1773+
1774+
const server2Connection = await connectionProvider.acquireConnection({
1775+
accessMode: 'WRITE',
1776+
database: 'databaseA'
1777+
})
1778+
1779+
const serverAConnection = await connectionProvider.acquireConnection({
1780+
accessMode: 'READ',
1781+
database: 'databaseB'
1782+
})
1783+
1784+
serverAConnection.handleAndTransformError(error, serverA)
1785+
server2Connection.handleAndTransformError(error, server2)
1786+
1787+
expect(pool.purge).toHaveBeenCalledWith(serverA)
1788+
expect(pool.purge).toHaveBeenCalledWith(server2)
1789+
})
1790+
1791+
it('should not change error when TokenExpired happens', async () => {
1792+
const pool = newPool()
1793+
1794+
const connectionProvider = newRoutingConnectionProvider(
1795+
[
1796+
newRoutingTable(
1797+
'databaseA',
1798+
[server1, server2],
1799+
[server1],
1800+
[server2]
1801+
),
1802+
newRoutingTable('databaseB', [serverA, serverB], [serverA], [serverB])
1803+
],
1804+
pool
1805+
)
1806+
1807+
const expectedError = newError(
1808+
'Message',
1809+
'Neo.ClientError.Security.TokenExpired'
1810+
)
1811+
1812+
const server2Connection = await connectionProvider.acquireConnection({
1813+
accessMode: 'WRITE',
1814+
database: 'databaseA'
1815+
})
1816+
1817+
const error = server2Connection.handleAndTransformError(
1818+
expectedError,
1819+
server2
1820+
)
1821+
1822+
expect(error).toBe(expectedError)
1823+
})
1824+
16771825
it('should acquire write connection from correct routing table', async () => {
16781826
const pool = newPool()
16791827
const connectionProvider = newRoutingConnectionProvider(

packages/bolt-connection/test/connection/connection-channel.test.js

+75-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*/
1919

2020
import ChannelConnection from '../../src/connection/connection-channel'
21-
import { int, internal } from 'neo4j-driver-core'
21+
import { int, internal, newError } from 'neo4j-driver-core'
2222
import { add } from 'lodash'
2323

2424
const {
@@ -127,6 +127,80 @@ describe('ChannelConnection', () => {
127127
)
128128
})
129129

130+
describe('._handleFatalError()', () => {
131+
describe('when there is not current failure on going', () => {
132+
const thrownError = newError('some error', 'C')
133+
let notifyFatalError;
134+
let connection;
135+
136+
beforeEach(() => {
137+
notifyFatalError = jest.fn()
138+
const protocol = {
139+
notifyFatalError,
140+
currentFailure: null
141+
}
142+
143+
const protocolSupplier = () => protocol
144+
connection = spyOnConnectionChannel({ protocolSupplier })
145+
})
146+
147+
it('should set connection state to broken', () => {
148+
connection._handleFatalError(thrownError)
149+
150+
expect(connection._isBroken).toBe(true)
151+
})
152+
153+
it('should set internal erro to the thrownError', () => {
154+
connection._handleFatalError(thrownError)
155+
156+
expect(connection._error).toBe(thrownError)
157+
})
158+
159+
it('should call notifyFatalError with the thrownError', () => {
160+
connection._handleFatalError(thrownError)
161+
162+
expect(notifyFatalError).toHaveBeenCalledWith(thrownError)
163+
})
164+
})
165+
166+
describe('when there is current failure on going', () => {
167+
const thrownError = newError('some error', 'C')
168+
const currentFailure = newError('current failure', 'ongoing')
169+
let notifyFatalError;
170+
let connection;
171+
172+
beforeEach(() => {
173+
notifyFatalError = jest.fn()
174+
const protocol = {
175+
notifyFatalError,
176+
currentFailure
177+
}
178+
179+
const protocolSupplier = () => protocol
180+
connection = spyOnConnectionChannel({ protocolSupplier })
181+
})
182+
183+
it('should set connection state to broken', () => {
184+
connection._handleFatalError(thrownError)
185+
186+
expect(connection._isBroken).toBe(true)
187+
})
188+
189+
it('should set internal erro to the currentFailure', () => {
190+
connection._handleFatalError(thrownError)
191+
192+
expect(connection._error).toBe(currentFailure)
193+
})
194+
195+
it('should call notifyFatalError with the currentFailure', () => {
196+
connection._handleFatalError(thrownError)
197+
198+
expect(notifyFatalError).toHaveBeenCalledWith(currentFailure)
199+
})
200+
})
201+
202+
})
203+
130204
function spyOnConnectionChannel ({
131205
channel,
132206
errorHandler,

0 commit comments

Comments
 (0)