diff --git a/.github/actions/cache-node-modules/action.yml b/.github/actions/cache-node-modules/action.yml
new file mode 100644
index 0000000000..efbffb8b5f
--- /dev/null
+++ b/.github/actions/cache-node-modules/action.yml
@@ -0,0 +1,22 @@
+name: Cache Node Modules
+description: Cache Node Modules
+
+inputs:
+ NODE_VERSION:
+ description: node version
+ required: true
+
+runs:
+ using: "composite"
+ steps:
+ - name: Cache node modules
+ uses: actions/cache@v2
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: node_modules
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ inputs.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-${{ inputs.NODE_VERSION }}-${{ hashFiles('package-lock.json') }}
+
+
diff --git a/.github/actions/functional-tests/action.yml b/.github/actions/functional-tests/action.yml
index 6913f90609..bbe78f7c0d 100644
--- a/.github/actions/functional-tests/action.yml
+++ b/.github/actions/functional-tests/action.yml
@@ -18,5 +18,4 @@ runs:
env:
KUZZLE_FUNCTIONAL_TESTS: ${{ inputs.test-set }}
NODE_VERSION: ${{ inputs.node-version }}
- REBUILD: "true"
shell: bash
diff --git a/.github/actions/unit-tests/action.yml b/.github/actions/unit-tests/action.yml
index 640eebefbf..e233a9704c 100644
--- a/.github/actions/unit-tests/action.yml
+++ b/.github/actions/unit-tests/action.yml
@@ -6,8 +6,6 @@ runs:
steps:
- run: npm install
shell: bash
- - run: npm rebuild
- shell: bash
- run: npm run build
shell: bash
- run: npm run test:unit:coverage
diff --git a/.github/workflows/pull_request.workflow.yml b/.github/workflows/pull_request.workflow.yml
index c0fb0a686e..76cd3bec9d 100644
--- a/.github/workflows/pull_request.workflow.yml
+++ b/.github/workflows/pull_request.workflow.yml
@@ -11,6 +11,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/es-lint
unit-tests:
@@ -25,6 +28,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/unit-tests
functional-tests-legacy:
@@ -40,6 +46,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/functional-tests
with:
test-set: test:functional:legacy:${{ matrix.test_set }}
@@ -58,6 +67,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/functional-tests
with:
test-set: test:functional:${{ matrix.test_set }}
@@ -89,6 +101,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/monkey-tests
with:
node-version: ${{ matrix.node-version }}
diff --git a/.github/workflows/push_dev.workflow.yml b/.github/workflows/push_dev.workflow.yml
index a8abe9422d..1d38c063bb 100644
--- a/.github/workflows/push_dev.workflow.yml
+++ b/.github/workflows/push_dev.workflow.yml
@@ -14,6 +14,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/es-lint
unit-tests:
@@ -28,6 +31,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/unit-tests
functional-tests-legacy:
@@ -43,6 +49,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/functional-tests
with:
test-set: test:functional:legacy:${{ matrix.test_set }}
@@ -61,6 +70,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/functional-tests
with:
test-set: test:functional:${{ matrix.test_set }}
@@ -83,6 +95,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/monkey-tests
with:
node-version: ${{ matrix.node-version }}
@@ -105,6 +120,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/deploy-doc
with:
REGION: us-west-2
diff --git a/.github/workflows/push_master.workflow.yml b/.github/workflows/push_master.workflow.yml
index e31a36d33e..365e00633f 100644
--- a/.github/workflows/push_master.workflow.yml
+++ b/.github/workflows/push_master.workflow.yml
@@ -14,6 +14,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/es-lint
unit-tests:
@@ -28,6 +31,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/unit-tests
functional-tests-legacy:
@@ -43,6 +49,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/functional-tests
with:
test-set: test:functional:legacy:${{ matrix.test_set }}
@@ -61,6 +70,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/functional-tests
with:
test-set: test:functional:${{ matrix.test_set }}
@@ -83,6 +95,9 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
+ - uses: ./.github/actions/cache-node-modules
+ with:
+ NODE_VERSION: ${{ matrix.node-version }}
- uses: ./.github/actions/monkey-tests
with:
node-version: ${{ matrix.node-version }}
diff --git a/.kuzzlerc.sample b/.kuzzlerc.sample
index 06376dc791..f5d3292886 100644
--- a/.kuzzlerc.sample
+++ b/.kuzzlerc.sample
@@ -105,6 +105,11 @@
// (see https://docs.kuzzle.io/core/2/guides/write-plugins)
"plugins": {
// [Common]
+ // * failsafeMode
+ // If true, Kuzzle will not load custom plugin and features (including
+ // the ones defined in the application).
+ // The API will only be available to administrators ("admin" profile)
+ // during failsafe mode.
// * bootstrapLockTimeout
// Maximum amount of time (in milliseconds)
// to wait for a concurrent plugin bootstrap
@@ -131,6 +136,7 @@
// Maximum number of pipes that can be delayed. If full, new pipes
// are rejected.
"common": {
+ "failsafeMode": false,
"bootstrapLockTimeout": 30000,
"pipeWarnTime": 40,
"initTimeout": 2000,
diff --git a/doc/2/api/errors/error-codes/api/index.md b/doc/2/api/errors/error-codes/api/index.md
index 427155ba22..8442b1234b 100644
--- a/doc/2/api/errors/error-codes/api/index.md
+++ b/doc/2/api/errors/error-codes/api/index.md
@@ -50,5 +50,6 @@ description: Error codes definitions
| api.process.incomplete_multiple_request
0x0202000a| [MultipleErrorsError](/core/2/api/errors/error-codes#multipleerrorserror)
(400)| At least one of the %s actions failed. | Failed to execute some or all actions requested | | api.process.not_enough_nodes
0x0202000b| [ServiceUnavailableError](/core/2/api/errors/error-codes#serviceunavailableerror)
(503)| Rejected: this cluster is disabled because there aren't enough nodes connected. | The Kuzzle cluster has not enough nodes available, and no new requests can be processed until new nodes are added | | api.process.unauthorized_origin
0x0202000c| [UnauthorizedError](/core/2/api/errors/error-codes#unauthorizederror)
(401)| The origin "%s" is not authorized. | The domain reaching out to Kuzzle is not authorized | +| api.process.too_many_logins_requests
0x0202000d| [TooManyRequestsError](/core/2/api/errors/error-codes#toomanyrequestserror)
(429)| Rejected: Too many login attempts per second | The request was denied because the maximum ("limits.loginsPerSecond") number of login attempts per second has been exceeded. | --- diff --git a/doc/2/api/errors/error-codes/plugin/index.md b/doc/2/api/errors/error-codes/plugin/index.md index 106f3728bd..3e62cb45cc 100644 --- a/doc/2/api/errors/error-codes/plugin/index.md +++ b/doc/2/api/errors/error-codes/plugin/index.md @@ -39,7 +39,7 @@ description: Error codes definitions | id / code | class / status | message | description | | --------- | -------------- | --------| ----------- | -| plugin.runtime.failed_init
0x04020001| [PluginImplementationError](/core/2/api/errors/error-codes#pluginimplementationerror)
(500)| Something went wrong during initialization of "%s" plugin. | An exception was thrown by a plugin's init function | +| plugin.runtime.failed_init
0x04020001| [PluginImplementationError](/core/2/api/errors/error-codes#pluginimplementationerror)
(500)| Something went wrong during initialization of "%s" plugin. Set "plugins.common.failsafeMode" to true to bypass plugin initialization. | An exception was thrown by a plugin's init function | | plugin.runtime.unexpected_error
0x04020002| [PluginImplementationError](/core/2/api/errors/error-codes#pluginimplementationerror)
(500)| Caught an unexpected plugin error: %s | Embeds an unexpected plugin error into a standardized KuzzleError object | | plugin.runtime.pipe_timeout
0x04020003
(504)| Plugin "%s": timeout error. A pipe on the event "%s" exceeded the timeout delay (%sms). Aborting. | A pipe function execution took more than the configured server limit | | plugin.runtime.too_many_pipes
0x04020004| [ServiceUnavailableError](/core/2/api/errors/error-codes#serviceunavailableerror)
(503)| Request discarded: maximum number of executing pipe functions reached. | The number of running pipes exceeds the configured capacity (see configuration files). This may be caused by pipes being too slow, or by an insufficient number of Kuzzle nodes. | diff --git a/doc/2/api/errors/error-codes/security/index.md b/doc/2/api/errors/error-codes/security/index.md index 5fa73ac4d8..436f1d9a79 100644 --- a/doc/2/api/errors/error-codes/security/index.md +++ b/doc/2/api/errors/error-codes/security/index.md @@ -45,6 +45,7 @@ description: Error codes definitions | --------- | -------------- | --------| ----------- | | security.rights.unauthorized
0x07030001| [UnauthorizedError](/core/2/api/errors/error-codes#unauthorizederror)
(401)| Unauthorized: authentication required to execute the action "%s:%s". | Authentication required to execute this action | | security.rights.forbidden
0x07030002| [ForbiddenError](/core/2/api/errors/error-codes#forbiddenerror)
(403)| Insufficient permissions to execute the action "%s:%s" (User "%s"). | Insufficient permissions to execute this action | +| security.rights.failsafe_mode_admin_only
0x07030003| [ForbiddenError](/core/2/api/errors/error-codes#forbiddenerror)
(403)| Only administrators ("admin" profile) can use the API in failsafe mode. | Only administrators ("admin" profile) can use the API in failsafe mode. Authenticate as admin or reboot without failsafe mode ("config.plugins.common.failsafeMode") to access the API. | --- diff --git a/features-legacy/support/api/mqtt.js b/features-legacy/support/api/mqtt.js index d02f7e57cf..d39514258a 100644 --- a/features-legacy/support/api/mqtt.js +++ b/features-legacy/support/api/mqtt.js @@ -120,6 +120,9 @@ class MqttApi extends ApiBase { } } else { + if (message.type === 'TokenExpired') { + this.responses = message; + } // notification const channel = topic; const roomId = topic.split('-')[0]; diff --git a/features-legacy/support/api/websocket.js b/features-legacy/support/api/websocket.js index f0eb53186d..910d117422 100644 --- a/features-legacy/support/api/websocket.js +++ b/features-legacy/support/api/websocket.js @@ -35,6 +35,9 @@ class WebSocketApi extends WsApiBase { const data = JSON.parse(message); if (data.scope || data.type === 'user' || data.type === 'TokenExpired') { + if (data.type === 'TokenExpired') { + this.responses = data; + } // notification const channel = data.room; const roomId = channel.split('-')[0]; diff --git a/lib/api/funnel.js b/lib/api/funnel.js index c89a8c50ac..d95ce32cee 100644 --- a/lib/api/funnel.js +++ b/lib/api/funnel.js @@ -50,6 +50,10 @@ const debug = require('../util/debug')('kuzzle:funnel'); const processError = kerror.wrap('api', 'process'); const { has } = require('../util/safeObject'); +// Actions of the auth controller that does not necessite to verify the token +// when cookie auth is active +const SKIP_TOKEN_VERIF_ACTIONS = ['login', 'checkToken', 'logout']; + /** * @class PendingRequest * @param {Request} request @@ -303,6 +307,9 @@ class Funnel { }) .then(allowed => { if (!allowed) { + if (request.input.controller === 'auth' && request.input.action === 'login') { + throw processError.get('too_many_logins_requests'); + } throw processError.get('too_many_requests'); } @@ -439,18 +446,18 @@ class Funnel { skipTokenVerification = request.getBoolean('cookieAuth') && request.input.controller === 'auth' - && ( request.input.action === 'login' - || request.input.action === 'checkToken' - || request.input.action === 'logout' - ); + && SKIP_TOKEN_VERIF_ACTIONS.includes(request.input.action); } } try { - // If the verification should be skipped, we pass a null token, this way the verification will be made as anonymous + // If the verification should be skipped, we pass a null token, + // this way the verification will be made as anonymous + const token = skipTokenVerification ? null : request.input.jwt; + request.context.token = await global.kuzzle.ask( 'core:security:token:verify', - !skipTokenVerification && request.input.jwt || null); + token); } catch (error) { await global.kuzzle.pipe('request:onUnauthorized', request); @@ -487,9 +494,21 @@ class Funnel { throw error; } + if ( global.kuzzle.config.plugins.common.failsafeMode + && ! this._isLogin(request) + && ! request.context.user.profileIds.includes('admin') + ) { + await global.kuzzle.pipe('request:onUnauthorized', request); + throw kerror.get('security', 'rights', 'failsafe_mode_admin_only'); + } + return global.kuzzle.pipe('request:onAuthorized', request); } + _isLogin (request) { + return request.input.controller === 'auth' && request.input.action === 'login'; + } + /** * Executes the request immediately. * /!\ To be used only by methods having already passed the overload check. diff --git a/lib/cluster/node.js b/lib/cluster/node.js index edc2e76983..06c1ce672a 100644 --- a/lib/cluster/node.js +++ b/lib/cluster/node.js @@ -375,7 +375,7 @@ class ClusterNode { // The goal of this process is to force at least 1 node to kill itself, // with all nodes concluding on their own on the same list of nodes to // shut down. - + // First remove every non existing node from topologies splits = splits.map( topology => topology.filter( @@ -462,24 +462,28 @@ class ClusterNode { timeout: this.config.joinTimeout, }); - await mutex.lock(); - try { + await mutex.lock(); + // Create the ID Key AFTER the handshake mutex is actually locked, // to prevent race conditions (other nodes attempting to connect to this // node while it's still initializing) await this.idCardHandler.createIdCard(); + global.kuzzle.log.debug('[CLUSTER] ID Card created'); this.nodeId = this.idCardHandler.nodeId; await this.startHeartbeat(); + global.kuzzle.log.debug('[CLUSTER] Start heartbeat'); let retried = false; let fullState = null; let nodes; + global.kuzzle.log.debug('[CLUSTER] Start retrieving full state..'); do { nodes = await this.idCardHandler.getRemoteIdCards(); + global.kuzzle.log.debug(`[CLUSTER] ${nodes.length} remote nodes discovered`); // No other nodes detected = no handshake required if (nodes.length === 0) { @@ -502,6 +506,7 @@ class ClusterNode { this.remoteNodes.set(id, subscriber); return subscriber.init(); }); + global.kuzzle.log.debug('[CLUSTER] Successfully subscribed to nodes'); fullState = await this.command.getFullState(nodes); @@ -531,14 +536,19 @@ class ClusterNode { } } while (fullState === null); + global.kuzzle.log.debug('[CLUSTER] Fullstate retrieved, loading into node..'); await this.fullState.loadFullState(fullState); this.activity = fullState.activity ? fullState.activity : this.activity; + global.kuzzle.log.debug('[CLUSTER] Fullstate loaded.'); + const handshakeResponses = await this.command.broadcastHandshake(nodes); + global.kuzzle.log.debug('[CLUSTER] Successful handshakes with other nodes.'); + // Update subscribers: start synchronizing, or unsubscribes from nodes who // didn't respond for (const [nodeId, handshakeData] of Object.entries(handshakeResponses)) { @@ -549,7 +559,7 @@ class ClusterNode { } else { await this.idCardHandler.addNode(nodeId); - const nodesStates = fullState.nodesState || []; + const nodesStates = fullState.nodesState || []; const nodeStatus = nodesStates.find(node => node.id === nodeId); subscriber.sync(nodeStatus ? nodeStatus.lastMessageId diff --git a/lib/cluster/workers/IDCardRenewer.js b/lib/cluster/workers/IDCardRenewer.js index 1a1c8db596..b0a87c7ca9 100644 --- a/lib/cluster/workers/IDCardRenewer.js +++ b/lib/cluster/workers/IDCardRenewer.js @@ -11,10 +11,14 @@ class IDCardRenewer { this.refreshTimer = null; this.nodeIdKey = null; this.refreshDelay = 2000; - this.disposeed = false; + this.disposed = true; // Disposed until initialized } async init (config) { + if (!this.disposed) { + return; // Already intialized + } + this.disposed = false; this.nodeIdKey = config.nodeIdKey; this.refreshDelay = config.refreshDelay || 2000; @@ -52,17 +56,19 @@ class IDCardRenewer { } async renewIDCard () { + if (this.disposed) { + return; // Do not refresh ID Card when worker has been disposed + } + try { - if (!this.disposed) { - const refreshed = await this.redis.commands.pexpire( - this.nodeIdKey, - this.refreshDelay * 3); - // Unable to refresh the key in time before it expires - // => this node is too slow, we need to remove it from the cluster - if (refreshed === 0) { - await this.dispose(); - this.parentPort.postMessage({ error: 'Node too slow: ID card expired' }); - } + const refreshed = await this.redis.commands.pexpire( + this.nodeIdKey, + this.refreshDelay * 3); + // Unable to refresh the key in time before it expires + // => this node is too slow, we need to remove it from the cluster + if (refreshed === 0) { + await this.dispose(); + this.parentPort.postMessage({ error: 'Node too slow: ID card expired' }); } } catch (error) { @@ -74,17 +80,19 @@ class IDCardRenewer { } async dispose () { - if (!this.disposed) { - this.disposed = true; - clearInterval(this.refreshTimer); - this.refreshTimer = null; - try { - await this.redis.commands.del(this.nodeIdKey); - } - catch (error) { - // eslint-disable-next-line no-console - console.error(`Could not delete key ${this.nodeIdKey} from redis: ${error.message}`); - } + if (this.disposed) { + return; // Already disposed + } + + this.disposed = true; + clearInterval(this.refreshTimer); + this.refreshTimer = null; + try { + await this.redis.commands.del(this.nodeIdKey); + } + catch (error) { + // eslint-disable-next-line no-console + console.error(`Could not delete key '${this.nodeIdKey}' from redis: ${error.message}`); } } } diff --git a/lib/config/default.config.js b/lib/config/default.config.js index 097c1058ab..d03eafa997 100644 --- a/lib/config/default.config.js +++ b/lib/config/default.config.js @@ -70,6 +70,7 @@ module.exports = { plugins: { common: { + failsafeMode: false, bootstrapLockTimeout: 30000, pipeWarnTime: 500, initTimeout: 10000, diff --git a/lib/core/plugin/pluginsManager.js b/lib/core/plugin/pluginsManager.js index 482a1af8da..df36746d2f 100644 --- a/lib/core/plugin/pluginsManager.js +++ b/lib/core/plugin/pluginsManager.js @@ -42,6 +42,9 @@ const runtimeError = kerror.wrap('plugin', 'runtime'); const strategyError = kerror.wrap('plugin', 'strategy'); const controllerError = kerror.wrap('plugin', 'controller'); +// Without those plugins, Kuzzle won't start at all. +const CORE_PLUGINS = ['kuzzle-plugin-logger', 'kuzzle-plugin-auth-passport-local']; + /** * @class PluginsManager * @param {Kuzzle} kuzzle @@ -89,6 +92,8 @@ class PluginsManager { if (this.config.common.pipeTimeout) { global.kuzzle.log.warn('The configuration "plugins.common.pipeTimeout" has been deprecated and is now unused. It can be safely removed from configuration files'); } + + this.loadedPlugins = []; } set application (plugin) { @@ -199,6 +204,11 @@ class PluginsManager { const loadPlugins = []; for (const plugin of this._plugins.values()) { + if (this.config.common.failsafeMode && ! CORE_PLUGINS.includes(plugin.name)) { + global.kuzzle.log.info(`Failsafe mode activated, skipping plugin "${plugin.name}"`); + continue; + } + if (plugin.application) { plugin.init(plugin.name); } @@ -255,6 +265,8 @@ class PluginsManager { debug('[%s] plugin started', plugin.name); + this.loadedPlugins.push(plugin.name); + return null; }); diff --git a/lib/core/realtime/notifier.js b/lib/core/realtime/notifier.js index 363af04734..bf59e48a8f 100644 --- a/lib/core/realtime/notifier.js +++ b/lib/core/realtime/notifier.js @@ -33,6 +33,13 @@ const { UserNotification, } = require('./notification'); +/** + * Notification are meant to be dispatched on "channels" created when subscribing. + * But some notification like TokenExpired don't have a specific channel to be received on, + * so we need a constant channel name to send Kuzzle notification without having to subscribe. + */ +const KUZZLE_NOTIFICATION_CHANNEL = 'kuzzle:notification:server'; + /** * @typedef {Object} DocumentChanges * @property {string} _id of the document @@ -178,30 +185,13 @@ class NotifierController { * @returns {Promise} */ async notifyTokenExpired (connectionId) { - const rooms = this.module.hotelClerk.getUserRooms(connectionId); - - if (rooms.length === 0) { - return; - } - - const channels = []; - - for (const room of rooms) { - const hotelClerkRoom = this.module.hotelClerk.rooms.get(room); - - if (hotelClerkRoom !== undefined) { - channels.push(...Object.keys(hotelClerkRoom.channels)); - } - } - - if (channels.length > 0) { - await this._dispatch( - 'notify:server', - channels, - new ServerNotification('TokenExpired', 'Authentication Token Expired'), - connectionId); - } - + + await this._dispatch( + 'notify:server', + [KUZZLE_NOTIFICATION_CHANNEL], // Sending notification on Kuzzle notification channel + new ServerNotification('TokenExpired', 'Authentication Token Expired'), + connectionId); + await this.module.hotelClerk.removeUser(connectionId); } diff --git a/lib/kerror/codes/2-api.json b/lib/kerror/codes/2-api.json index 8f040630b7..bbc07bb187 100644 --- a/lib/kerror/codes/2-api.json +++ b/lib/kerror/codes/2-api.json @@ -164,6 +164,12 @@ "code": 12, "message": "The origin \"%s\" is not authorized.", "class": "UnauthorizedError" + }, + "too_many_logins_requests": { + "description": "The request was denied because the maximum (\"limits.loginsPerSecond\") number of login attempts per second has been exceeded.", + "code": 13, + "message": "Rejected: Too many login attempts per second", + "class": "TooManyRequestsError" } } } diff --git a/lib/kerror/codes/4-plugin.json b/lib/kerror/codes/4-plugin.json index 27c60cb67a..9c727454cd 100644 --- a/lib/kerror/codes/4-plugin.json +++ b/lib/kerror/codes/4-plugin.json @@ -102,7 +102,7 @@ "failed_init": { "description": "An exception was thrown by a plugin's init function", "code": 1, - "message": "Something went wrong during initialization of \"%s\" plugin.", + "message": "Something went wrong during initialization of \"%s\" plugin. Set \"plugins.common.failsafeMode\" to true to bypass plugin initialization.", "class": "PluginImplementationError" }, "unexpected_error": { diff --git a/lib/kerror/codes/7-security.json b/lib/kerror/codes/7-security.json index ca2431c3b6..13fb9c43bb 100644 --- a/lib/kerror/codes/7-security.json +++ b/lib/kerror/codes/7-security.json @@ -91,6 +91,12 @@ "code": 2, "message": "Insufficient permissions to execute the action \"%s:%s\" (User \"%s\").", "class": "ForbiddenError" + }, + "failsafe_mode_admin_only": { + "description": "Only administrators (\"admin\" profile) can use the API in failsafe mode. Authenticate as admin or reboot without failsafe mode (\"config.plugins.common.failsafeMode\") to access the API.", + "code": 3, + "message": "Only administrators (\"admin\" profile) can use the API in failsafe mode.", + "class": "ForbiddenError" } } }, diff --git a/lib/kuzzle/kuzzle.ts b/lib/kuzzle/kuzzle.ts index d652b11126..673fb41022 100644 --- a/lib/kuzzle/kuzzle.ts +++ b/lib/kuzzle/kuzzle.ts @@ -125,7 +125,7 @@ class Kuzzle extends KuzzleEventEmitter { * Validation core component */ private validation: Validation; - + /** * Dump generator */ @@ -177,7 +177,7 @@ class Kuzzle extends KuzzleEventEmitter { this.log = new Logger(); this.rootPath = path.resolve(path.join(__dirname, '../..')); - + this.internalIndex = new InternalIndexHandler(); this.pluginsManager = new PluginsManager(); this.tokenManager = new TokenManager(); @@ -214,7 +214,7 @@ class Kuzzle extends KuzzleEventEmitter { try { this.log.info(`[ℹ] Starting Kuzzle ${this.version} ...`); await this.pipe('kuzzle:state:start'); - + // Koncorde realtime engine this.koncorde = new Koncorde({ @@ -253,7 +253,7 @@ class Kuzzle extends KuzzleEventEmitter { this.pluginsManager.application = application; await this.pluginsManager.init(options.plugins); - this.log.info(`[✔] Successfully loaded ${this.pluginsManager.plugins.length} plugins: ${this.pluginsManager.plugins.map(p => p.name).join(', ')}`); + this.log.info(`[✔] Successfully loaded ${this.pluginsManager.loadedPlugins.length} plugins: ${this.pluginsManager.loadedPlugins.join(', ')}`); // Authentification plugins must be loaded before users import to avoid // credentials related error which would prevent Kuzzle from starting @@ -469,7 +469,7 @@ class Kuzzle extends KuzzleEventEmitter { } const toSupport = config.toSupport; - + if (! _.isEmpty(toSupport.fixtures)) { await this.ask('core:storage:public:document:import', toSupport.fixtures); this.log.info('[✔] Fixtures import successful'); @@ -544,7 +544,7 @@ class Kuzzle extends KuzzleEventEmitter { importTypes.shift(); continue; } - + await Bluebird.delay(1000); } } @@ -568,7 +568,7 @@ class Kuzzle extends KuzzleEventEmitter { const mutex = new Mutex(`backend:import:${type}`, { timeout: 0 }); const initialized = await this.ask('core:cache:internal:get', `${BACKEND_IMPORT_KEY}:${type}`) === '1'; const locked = await mutex.lock(); - + await importMethod( { toImport, toSupport }, { @@ -584,7 +584,7 @@ class Kuzzle extends KuzzleEventEmitter { } } - + this.log.info('[✔] Waiting for imports to be finished'); await this._waitForImportToFinish(); this.log.info('[✔] Import successful'); diff --git a/lib/kuzzle/log.js b/lib/kuzzle/log.js index 948999e287..3a82ea24e9 100644 --- a/lib/kuzzle/log.js +++ b/lib/kuzzle/log.js @@ -25,6 +25,10 @@ class Logger { constructor() { this.logMethods = ['info', 'warn', 'error', 'silly', 'debug', 'verbose']; + this.failsafeModeString = global.kuzzle.config.plugins.common.failsafeMode + ? '[FAILSAFE MODE] ' + : ''; + this._useConsole(); global.kuzzle.once('core:kuzzleStart', this._useLogger.bind(this)); @@ -33,12 +37,17 @@ class Logger { _useConsole() { // until kuzzle has started, use the console to print logs for (const method of this.logMethods) { - /* eslint-disable-next-line no-console */ - this[method] = console[method] || console.log; + this[method] = message => { + /* eslint-disable-next-line no-console */ + const writer = console[method] || console.log; + + writer(`${this.failsafeModeString}${message}`); + }; } } _useLogger() { + // when kuzzle has started, use the event to dispatch logs for (const method of this.logMethods) { this[method] = message => { @@ -47,10 +56,10 @@ class Logger { ) { const request = global.kuzzle.asyncStore.get('REQUEST'); - global.kuzzle.emit(`log:${method}`, `[${global.kuzzle.id}] [${request.id}] ${message}`); + global.kuzzle.emit(`log:${method}`, `[${global.kuzzle.id}] ${this.failsafeModeString}[${request.id}] ${message}`); } else { - global.kuzzle.emit(`log:${method}`, `[${global.kuzzle.id}] ${message}`); + global.kuzzle.emit(`log:${method}`, `[${global.kuzzle.id}] ${this.failsafeModeString}${message}`); } }; } diff --git a/package-lock.json b/package-lock.json index 30579a662f..76091a84c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kuzzle", - "version": "2.14.7", + "version": "2.14.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3d9715441e..8758c66496 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kuzzle", "author": "The Kuzzle Team