kuzzle 2.60.0-beta.3 → 2.60.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/api/controllers/adminController.d.ts +7 -1
- package/dist/lib/api/controllers/adminController.js +9 -2
- package/dist/lib/api/controllers/documentController.d.ts +1 -1
- package/dist/lib/api/funnel.js +12 -1
- package/dist/lib/api/httpRoutes.js +52 -20
- package/dist/lib/api/request/kuzzleRequest.d.ts +7 -2
- package/dist/lib/api/request/kuzzleRequest.js +20 -10
- package/dist/lib/api/request/requestContext.d.ts +1 -1
- package/dist/lib/api/request/requestResponse.js +2 -1
- package/dist/lib/cluster/command.d.ts +7 -0
- package/dist/lib/cluster/command.js +67 -28
- package/dist/lib/cluster/node.d.ts +13 -2
- package/dist/lib/cluster/node.js +20 -4
- package/dist/lib/cluster/subscriber.d.ts +2 -0
- package/dist/lib/cluster/subscriber.js +6 -0
- package/dist/lib/config/index.js +60 -3
- package/dist/lib/core/backend/backend.d.ts +4 -3
- package/dist/lib/core/backend/backend.js +15 -15
- package/dist/lib/core/backend/backendCluster.d.ts +1 -1
- package/dist/lib/core/backend/internalLogger.d.ts +6 -0
- package/dist/lib/core/backend/internalLogger.js +6 -0
- package/dist/lib/core/network/router.d.ts +18 -0
- package/dist/lib/core/network/router.js +44 -10
- package/dist/lib/core/security/authTokenSettings.d.ts +21 -0
- package/dist/lib/core/security/authTokenSettings.js +61 -0
- package/dist/lib/core/security/tokenRepository.js +8 -14
- package/dist/lib/core/shared/KoncordeWrapper.d.ts +9 -0
- package/dist/lib/core/shared/KoncordeWrapper.js +8 -8
- package/dist/lib/core/shared/store.js +4 -0
- package/dist/lib/core/storage/clientAdapter.d.ts +6 -0
- package/dist/lib/core/storage/clientAdapter.js +40 -5
- package/dist/lib/kuzzle/Logger.d.ts +4 -0
- package/dist/lib/kuzzle/Logger.js +9 -2
- package/dist/lib/kuzzle/internalIndexHandler.js +28 -11
- package/dist/lib/kuzzle/kuzzle.d.ts +17 -0
- package/dist/lib/kuzzle/kuzzle.js +62 -0
- package/dist/lib/kuzzle/vault.d.ts +13 -0
- package/dist/lib/kuzzle/vault.js +25 -7
- package/dist/lib/model/security/profile.d.ts +1 -1
- package/dist/lib/model/security/role.d.ts +1 -1
- package/dist/lib/model/security/user.d.ts +1 -1
- package/dist/lib/service/storage/7/elasticsearch.d.ts +18 -0
- package/dist/lib/service/storage/7/elasticsearch.js +50 -10
- package/dist/lib/service/storage/7/esWrapper.js +31 -2
- package/dist/lib/service/storage/8/elasticsearch.d.ts +18 -0
- package/dist/lib/service/storage/8/elasticsearch.js +50 -10
- package/dist/lib/service/storage/8/esWrapper.js +31 -2
- package/dist/lib/types/config/KuzzleConfiguration.d.ts +7 -0
- package/dist/lib/types/config/SecurityConfiguration.d.ts +4 -3
- package/dist/lib/types/controllers/ControllerDefinition.d.ts +4 -4
- package/dist/lib/types/events/EventGenericDocument.d.ts +2 -1
- package/dist/lib/types/events/EventProtocol.d.ts +1 -1
- package/dist/lib/util/mutex.d.ts +5 -4
- package/dist/lib/util/mutex.js +8 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -44,7 +44,13 @@ declare class AdminController extends NativeController {
|
|
|
44
44
|
shutdown(): Promise<{
|
|
45
45
|
acknowledge: boolean;
|
|
46
46
|
}>;
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Always waits for the import, so that a failure is reported to the caller:
|
|
49
|
+
* `refresh=false` only skips waiting for the documents to be indexed.
|
|
50
|
+
*/
|
|
51
|
+
loadFixtures(request: KuzzleRequest): Promise<{
|
|
52
|
+
acknowledge: boolean;
|
|
53
|
+
}>;
|
|
48
54
|
loadMappings(request: KuzzleRequest): Promise<{
|
|
49
55
|
acknowledge: boolean;
|
|
50
56
|
}>;
|
|
@@ -161,15 +161,22 @@ class AdminController extends baseController_1.NativeController {
|
|
|
161
161
|
if (this.shuttingDown) {
|
|
162
162
|
throw kerror.get("api", "process", "action_locked", "Kuzzle is already shutting down.");
|
|
163
163
|
}
|
|
164
|
+
this.shuttingDown = true;
|
|
164
165
|
global.kuzzle.shutdown();
|
|
165
166
|
return { acknowledge: true };
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Always waits for the import, so that a failure is reported to the caller:
|
|
170
|
+
* `refresh=false` only skips waiting for the documents to be indexed.
|
|
171
|
+
*/
|
|
172
|
+
async loadFixtures(request) {
|
|
168
173
|
const fixtures = request.getBody();
|
|
169
174
|
const refresh = request.getRefresh("wait_for");
|
|
170
|
-
|
|
175
|
+
await global.kuzzle.ask("core:storage:public:document:import", fixtures, {
|
|
171
176
|
refresh,
|
|
172
177
|
});
|
|
178
|
+
// The documented answer. 2.17.6 to 2.59 answered `null` instead.
|
|
179
|
+
return { acknowledge: true };
|
|
173
180
|
}
|
|
174
181
|
loadMappings(request) {
|
|
175
182
|
const mappings = request.getBody();
|
|
@@ -77,7 +77,7 @@ declare class DocumentController extends NativeController {
|
|
|
77
77
|
errors: any;
|
|
78
78
|
successes: any;
|
|
79
79
|
}>;
|
|
80
|
-
export(request: KuzzleRequest): Promise<import("
|
|
80
|
+
export(request: KuzzleRequest): Promise<import("../../types").HttpStream>;
|
|
81
81
|
/**
|
|
82
82
|
* @param {Request} request
|
|
83
83
|
* @returns {Promise<Object>}
|
package/dist/lib/api/funnel.js
CHANGED
|
@@ -664,13 +664,24 @@ class Funnel {
|
|
|
664
664
|
catch (e) {
|
|
665
665
|
error = e;
|
|
666
666
|
debug("Error processing request %s: %a", request.id, error);
|
|
667
|
+
// processRequest does not dump: do it as the event-less path does.
|
|
668
|
+
// Rethrown, never falling through to the event-less execution below,
|
|
669
|
+
// which ran a failed (or pipe-rejected) request a second time.
|
|
670
|
+
this.handleErrorDump((0, thrown_1.causeOf)(e));
|
|
671
|
+
throw e;
|
|
667
672
|
}
|
|
668
673
|
finally {
|
|
669
|
-
|
|
674
|
+
// Not awaited, as it always was: the caller's result does not depend
|
|
675
|
+
// on it. Its rejection is logged instead of left unhandled.
|
|
676
|
+
global.kuzzle
|
|
677
|
+
.pipe("request:afterExecution", {
|
|
670
678
|
error: error,
|
|
671
679
|
request: request,
|
|
672
680
|
result: res,
|
|
673
681
|
success: error === undefined,
|
|
682
|
+
})
|
|
683
|
+
?.catch((pipeError) => {
|
|
684
|
+
this.logger.error(pipeError, `request:afterExecution pipe failed for plugin request ${request.id}`);
|
|
674
685
|
});
|
|
675
686
|
}
|
|
676
687
|
}
|
|
@@ -184,12 +184,6 @@ const routes = [
|
|
|
184
184
|
controller: "admin",
|
|
185
185
|
action: "resetDatabase",
|
|
186
186
|
},
|
|
187
|
-
{
|
|
188
|
-
verb: "post",
|
|
189
|
-
path: "/admin/_resetKuzzleData",
|
|
190
|
-
controller: "admin",
|
|
191
|
-
action: "resetKuzzleData",
|
|
192
|
-
},
|
|
193
187
|
{ verb: "post", path: "/admin/_dump", controller: "admin", action: "dump" },
|
|
194
188
|
{
|
|
195
189
|
verb: "post",
|
|
@@ -251,12 +245,6 @@ const routes = [
|
|
|
251
245
|
controller: "index",
|
|
252
246
|
action: "exists",
|
|
253
247
|
},
|
|
254
|
-
{
|
|
255
|
-
verb: "get",
|
|
256
|
-
path: "/:index/_autoRefresh",
|
|
257
|
-
controller: "index",
|
|
258
|
-
action: "getAutoRefresh",
|
|
259
|
-
},
|
|
260
248
|
{ verb: "get", path: "/_list", controller: "index", action: "list" },
|
|
261
249
|
{ verb: "get", path: "/_storageStats", controller: "index", action: "stats" },
|
|
262
250
|
{
|
|
@@ -1133,13 +1121,6 @@ const routes = [
|
|
|
1133
1121
|
controller: "ms",
|
|
1134
1122
|
action: "append",
|
|
1135
1123
|
},
|
|
1136
|
-
{
|
|
1137
|
-
verb: "post",
|
|
1138
|
-
path: "/ms/_bgrewriteaof",
|
|
1139
|
-
controller: "ms",
|
|
1140
|
-
action: "bgrewriteaof",
|
|
1141
|
-
},
|
|
1142
|
-
{ verb: "post", path: "/ms/_bgsave", controller: "ms", action: "bgsave" },
|
|
1143
1124
|
{ verb: "post", path: "/ms/_bitop/:_id", controller: "ms", action: "bitop" },
|
|
1144
1125
|
{ verb: "post", path: "/ms/_decr/:_id", controller: "ms", action: "decr" },
|
|
1145
1126
|
{
|
|
@@ -1514,7 +1495,7 @@ const routes = [
|
|
|
1514
1495
|
controller: "security",
|
|
1515
1496
|
action: "deleteApiKey",
|
|
1516
1497
|
openapi: {
|
|
1517
|
-
description: "Deletes a user API key, identified by its clear-text `key` or its `fingerprint` instead of its `_id`.
|
|
1498
|
+
description: "Deletes a user API key, identified by its clear-text `key` or its `fingerprint` instead of its `_id`. One of them must be provided; if several are given, `_id` is used first, then `key`, then `fingerprint`, and the others are ignored. A `key` in the query string is a secret written in the URL: prefer the `fingerprint`. If you are looking to manage your own API keys, use the `auth` controller instead.",
|
|
1518
1499
|
parameters: [
|
|
1519
1500
|
{
|
|
1520
1501
|
in: "path",
|
|
@@ -1572,6 +1553,57 @@ const routes = [
|
|
|
1572
1553
|
controller: "auth",
|
|
1573
1554
|
action: "deleteApiKey",
|
|
1574
1555
|
},
|
|
1556
|
+
{
|
|
1557
|
+
verb: "delete",
|
|
1558
|
+
path: "/api-keys",
|
|
1559
|
+
controller: "auth",
|
|
1560
|
+
action: "deleteApiKey",
|
|
1561
|
+
openapi: {
|
|
1562
|
+
description: "Deletes an API key of the current user, identified by its clear-text `key` or its `fingerprint` instead of its `_id`. One of them must be provided; if several are given, `_id` is used first, then `key`, then `fingerprint`, and the others are ignored. A `key` in the query string is a secret written in the URL: prefer the `fingerprint`.",
|
|
1563
|
+
parameters: [
|
|
1564
|
+
{
|
|
1565
|
+
in: "query",
|
|
1566
|
+
name: "key",
|
|
1567
|
+
schema: { type: "string" },
|
|
1568
|
+
required: false,
|
|
1569
|
+
description: "Clear-text API key to delete",
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
in: "query",
|
|
1573
|
+
name: "fingerprint",
|
|
1574
|
+
schema: { type: "string" },
|
|
1575
|
+
required: false,
|
|
1576
|
+
description: "SHA256 fingerprint of the API key to delete",
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
in: "query",
|
|
1580
|
+
name: "refresh",
|
|
1581
|
+
schema: {
|
|
1582
|
+
type: "string",
|
|
1583
|
+
enum: ["wait_for", "false"],
|
|
1584
|
+
default: "wait_for",
|
|
1585
|
+
},
|
|
1586
|
+
required: false,
|
|
1587
|
+
description: "If set to `wait_for`, waits for the deletion to be indexed before responding",
|
|
1588
|
+
},
|
|
1589
|
+
],
|
|
1590
|
+
responses: {
|
|
1591
|
+
200: {
|
|
1592
|
+
description: "The deleted API key ID",
|
|
1593
|
+
content: {
|
|
1594
|
+
"application/json": {
|
|
1595
|
+
schema: {
|
|
1596
|
+
type: "object",
|
|
1597
|
+
properties: {
|
|
1598
|
+
_id: { type: "string" },
|
|
1599
|
+
},
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
},
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1575
1607
|
// PUT (idempotent)
|
|
1576
1608
|
{ verb: "put", path: "/_me", controller: "auth", action: "updateSelf" },
|
|
1577
1609
|
{
|
|
@@ -385,12 +385,17 @@ export declare class KuzzleRequest {
|
|
|
385
385
|
*/
|
|
386
386
|
getDate(name: string, format?: string): string;
|
|
387
387
|
/**
|
|
388
|
-
* Gets a parameter from a request arguments and
|
|
388
|
+
* Gets a parameter from a request arguments and checks that it is an integer
|
|
389
|
+
* timestamp (e.g. milliseconds since Epoch).
|
|
390
|
+
*
|
|
391
|
+
* An integer string, such as an HTTP query string value, is accepted and
|
|
392
|
+
* converted. A date string (e.g. `"2026-10-27"`) is rejected.
|
|
389
393
|
*
|
|
390
394
|
* @param name parameter name.
|
|
391
395
|
* @throws {api.assert.missing_argument} If parameter not found and no default
|
|
392
396
|
* value provided
|
|
393
|
-
* @throws {api.assert.invalid_type} If parameter value is not
|
|
397
|
+
* @throws {api.assert.invalid_type} If parameter value is not an integer
|
|
398
|
+
* timestamp.
|
|
394
399
|
*/
|
|
395
400
|
getTimestamp(name: string): number;
|
|
396
401
|
/**
|
|
@@ -676,31 +676,41 @@ class KuzzleRequest {
|
|
|
676
676
|
if (args[name] === undefined) {
|
|
677
677
|
throw assertionError.get("missing_argument", name);
|
|
678
678
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
679
|
+
// A custom format replaces the ISO 8601 check: applying both rejected
|
|
680
|
+
// every value of a format that is not ISO 8601.
|
|
681
|
+
const isValid = format
|
|
682
|
+
? (0, moment_1.default)(args[name], format, true).isValid()
|
|
683
|
+
: (0, moment_1.default)(args[name], moment_1.default.ISO_8601).isValid();
|
|
684
|
+
if (!isValid) {
|
|
683
685
|
throw assertionError.get("invalid_type", name, "date");
|
|
684
686
|
}
|
|
685
687
|
return this.getString(name);
|
|
686
688
|
}
|
|
687
689
|
/**
|
|
688
|
-
* Gets a parameter from a request arguments and
|
|
690
|
+
* Gets a parameter from a request arguments and checks that it is an integer
|
|
691
|
+
* timestamp (e.g. milliseconds since Epoch).
|
|
692
|
+
*
|
|
693
|
+
* An integer string, such as an HTTP query string value, is accepted and
|
|
694
|
+
* converted. A date string (e.g. `"2026-10-27"`) is rejected.
|
|
689
695
|
*
|
|
690
696
|
* @param name parameter name.
|
|
691
697
|
* @throws {api.assert.missing_argument} If parameter not found and no default
|
|
692
698
|
* value provided
|
|
693
|
-
* @throws {api.assert.invalid_type} If parameter value is not
|
|
699
|
+
* @throws {api.assert.invalid_type} If parameter value is not an integer
|
|
700
|
+
* timestamp.
|
|
694
701
|
*/
|
|
695
702
|
getTimestamp(name) {
|
|
696
|
-
const
|
|
697
|
-
if (
|
|
703
|
+
const value = this.input.args[name];
|
|
704
|
+
if (value === undefined) {
|
|
698
705
|
throw assertionError.get("missing_argument", name);
|
|
699
706
|
}
|
|
700
|
-
|
|
707
|
+
const timestamp = typeof value === "string" && /^-?\d+$/.test(value)
|
|
708
|
+
? Number(value)
|
|
709
|
+
: value;
|
|
710
|
+
if (!Number.isSafeInteger(timestamp)) {
|
|
701
711
|
throw assertionError.get("invalid_type", name, "date");
|
|
702
712
|
}
|
|
703
|
-
return
|
|
713
|
+
return timestamp;
|
|
704
714
|
}
|
|
705
715
|
/**
|
|
706
716
|
* Returns the index specified in the request
|
|
@@ -127,7 +127,8 @@ class Headers {
|
|
|
127
127
|
case "host":
|
|
128
128
|
case "if-modified-since":
|
|
129
129
|
case "if-unmodified-since":
|
|
130
|
-
case "last-modified
|
|
130
|
+
case "last-modified":
|
|
131
|
+
case "location":
|
|
131
132
|
case "max-forwards":
|
|
132
133
|
case "proxy-authorization":
|
|
133
134
|
case "referer":
|
|
@@ -21,6 +21,8 @@ type CommandingNode = {
|
|
|
21
21
|
};
|
|
22
22
|
ip: string;
|
|
23
23
|
nodeId: string;
|
|
24
|
+
/** Whether `nodeId` can be read: false until the handshake sets it. */
|
|
25
|
+
hasNodeId: boolean;
|
|
24
26
|
activity: Activity[];
|
|
25
27
|
/** Only `serialize()`: the command layer never mutates the state. */
|
|
26
28
|
fullState: {
|
|
@@ -102,6 +104,11 @@ declare class ClusterCommand {
|
|
|
102
104
|
* This method can (and should) never return a rejected promise.
|
|
103
105
|
*/
|
|
104
106
|
listen(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Answers one request. Every request gets exactly one reply: REP/REQ
|
|
109
|
+
* requires it, and a request left unanswered wedges the peer's socket.
|
|
110
|
+
*/
|
|
111
|
+
private answer;
|
|
105
112
|
/**
|
|
106
113
|
* Closes opened sockets and marks this server as closed.
|
|
107
114
|
*/
|
|
@@ -159,32 +159,9 @@ class ClusterCommand {
|
|
|
159
159
|
while (this.state === stateEnum.RUNNING) {
|
|
160
160
|
try {
|
|
161
161
|
const [type, data] = await this.server.receive();
|
|
162
|
-
//
|
|
163
|
-
// request
|
|
164
|
-
|
|
165
|
-
switch (type?.toString()) {
|
|
166
|
-
case commandMessages_1.commandTopic.FULLSTATE:
|
|
167
|
-
await this.sendFullState();
|
|
168
|
-
break;
|
|
169
|
-
case commandMessages_1.commandTopic.HANDSHAKE:
|
|
170
|
-
if (data === undefined) {
|
|
171
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
await this.handleHandshake(data);
|
|
175
|
-
break;
|
|
176
|
-
case commandMessages_1.commandTopic.RETRANSMIT:
|
|
177
|
-
if (data === undefined) {
|
|
178
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
await this.handleRetransmit(data);
|
|
182
|
-
break;
|
|
183
|
-
default:
|
|
184
|
-
// REP/REQ sockets expect a reply to each request made, so we have
|
|
185
|
-
// to send some kind of response on an invalid request received
|
|
186
|
-
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
187
|
-
}
|
|
162
|
+
// NOSONAR (S9382): a REP socket strictly alternates receive and send,
|
|
163
|
+
// so a request must be answered before the next one can be read.
|
|
164
|
+
await this.answer(type, data); // NOSONAR
|
|
188
165
|
}
|
|
189
166
|
catch (e) {
|
|
190
167
|
if (this.state !== stateEnum.CLOSED) {
|
|
@@ -193,6 +170,47 @@ class ClusterCommand {
|
|
|
193
170
|
}
|
|
194
171
|
}
|
|
195
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Answers one request. Every request gets exactly one reply: REP/REQ
|
|
175
|
+
* requires it, and a request left unanswered wedges the peer's socket.
|
|
176
|
+
*/
|
|
177
|
+
async answer(type, data) {
|
|
178
|
+
// The socket is bound before the handshake sets this node's id, and a
|
|
179
|
+
// peer still talking to a dead node on this IP can reach it (#3031). A
|
|
180
|
+
// node with no id yet has no state to serve and cannot take a peer in:
|
|
181
|
+
// every request is answered DISCARDED, which the requester treats as no
|
|
182
|
+
// answer.
|
|
183
|
+
if (!this.node.hasNodeId) {
|
|
184
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// A REQ frame carries a topic and, for a handshake, a payload. A request
|
|
188
|
+
// missing either is answered with DISCARDED, which is what this socket
|
|
189
|
+
// owes every REQ — the two frames were read unchecked.
|
|
190
|
+
switch (type?.toString()) {
|
|
191
|
+
case commandMessages_1.commandTopic.FULLSTATE:
|
|
192
|
+
await this.sendFullState();
|
|
193
|
+
break;
|
|
194
|
+
case commandMessages_1.commandTopic.HANDSHAKE:
|
|
195
|
+
if (data === undefined) {
|
|
196
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
await this.handleHandshake(data);
|
|
200
|
+
break;
|
|
201
|
+
case commandMessages_1.commandTopic.RETRANSMIT:
|
|
202
|
+
if (data === undefined) {
|
|
203
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
await this.handleRetransmit(data);
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
// REP/REQ sockets expect a reply to each request made, so we have
|
|
210
|
+
// to send some kind of response on an invalid request received
|
|
211
|
+
await this.server.send([commandMessages_1.commandTopic.DISCARDED, null]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
196
214
|
/**
|
|
197
215
|
* Closes opened sockets and marks this server as closed.
|
|
198
216
|
*/
|
|
@@ -339,7 +357,19 @@ class ClusterCommand {
|
|
|
339
357
|
// No payload message for a full state request
|
|
340
358
|
await req.send([commandMessages_1.commandTopic.FULLSTATE, null]);
|
|
341
359
|
try {
|
|
342
|
-
|
|
360
|
+
// NOSONAR (S9382): one node at a time by design, the loop stops at
|
|
361
|
+
// the first one that serves a full state.
|
|
362
|
+
const [topic, payload] = await req.receive(); // NOSONAR
|
|
363
|
+
// DISCARDED carries an empty frame, which would decode as an empty
|
|
364
|
+
// full state: a node that cannot serve one (#3031) is skipped like
|
|
365
|
+
// one that does not answer.
|
|
366
|
+
if (topic?.toString() === commandMessages_1.commandTopic.FULLSTATE && payload) {
|
|
367
|
+
fullState = payload;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
this.logger.warn(`Unable to fetch a full state from node ${id} (request discarded)`);
|
|
371
|
+
idx = (idx + 1) % nodes.length;
|
|
372
|
+
}
|
|
343
373
|
}
|
|
344
374
|
catch {
|
|
345
375
|
// no response from the remote node in a timely fashion... retrying
|
|
@@ -403,7 +433,16 @@ class ClusterCommand {
|
|
|
403
433
|
await req.send([commandMessages_1.commandTopic.HANDSHAKE, payload]);
|
|
404
434
|
let response = null;
|
|
405
435
|
try {
|
|
406
|
-
[,
|
|
436
|
+
const [topic, answer] = await req.receive();
|
|
437
|
+
// DISCARDED carries an empty frame, which would decode as a handshake
|
|
438
|
+
// answer with nothing in it: a node that refused the handshake (#3031)
|
|
439
|
+
// is one that did not answer.
|
|
440
|
+
if (topic?.toString() === commandMessages_1.commandTopic.HANDSHAKE && answer) {
|
|
441
|
+
response = answer;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
this.logger.warn(`Couldn't complete handshake with node ${id}: request discarded`);
|
|
445
|
+
}
|
|
407
446
|
}
|
|
408
447
|
catch {
|
|
409
448
|
this.logger.warn(`Couldn't complete handshake with node ${id}: no response received`);
|
|
@@ -41,10 +41,21 @@ declare class ClusterNode {
|
|
|
41
41
|
* The id of the ID card this node reserved during `handshake()`.
|
|
42
42
|
*
|
|
43
43
|
* Null until the handshake gets that far, and read as a `string` at
|
|
44
|
-
* seventeen sites here plus `subscriber.ts` and `command.ts
|
|
45
|
-
*
|
|
44
|
+
* seventeen sites here plus `subscriber.ts` and `command.ts`. The accessor
|
|
45
|
+
* says once where that can fail: an internal read before the handshake is a
|
|
46
|
+
* bug, and it throws.
|
|
47
|
+
*
|
|
48
|
+
* Network input is not internal: the command socket is bound before the
|
|
49
|
+
* handshake, and a node restarted on the IP of a dead one can be reached by
|
|
50
|
+
* peers still talking to that dead node (#3031). The paths network input
|
|
51
|
+
* reaches check `hasNodeId` first.
|
|
46
52
|
*/
|
|
47
53
|
get nodeId(): string;
|
|
54
|
+
/**
|
|
55
|
+
* Whether the handshake has set this node's id yet: the non-throwing check
|
|
56
|
+
* for the paths network input reaches before it has (#3031).
|
|
57
|
+
*/
|
|
58
|
+
get hasNodeId(): boolean;
|
|
48
59
|
set nodeId(id: string);
|
|
49
60
|
private heartbeatTimer;
|
|
50
61
|
readonly idCardHandler: ClusterIdCardHandler;
|
package/dist/lib/cluster/node.js
CHANGED
|
@@ -200,8 +200,14 @@ class ClusterNode {
|
|
|
200
200
|
* The id of the ID card this node reserved during `handshake()`.
|
|
201
201
|
*
|
|
202
202
|
* Null until the handshake gets that far, and read as a `string` at
|
|
203
|
-
* seventeen sites here plus `subscriber.ts` and `command.ts
|
|
204
|
-
*
|
|
203
|
+
* seventeen sites here plus `subscriber.ts` and `command.ts`. The accessor
|
|
204
|
+
* says once where that can fail: an internal read before the handshake is a
|
|
205
|
+
* bug, and it throws.
|
|
206
|
+
*
|
|
207
|
+
* Network input is not internal: the command socket is bound before the
|
|
208
|
+
* handshake, and a node restarted on the IP of a dead one can be reached by
|
|
209
|
+
* peers still talking to that dead node (#3031). The paths network input
|
|
210
|
+
* reaches check `hasNodeId` first.
|
|
205
211
|
*/
|
|
206
212
|
get nodeId() {
|
|
207
213
|
if (this._nodeId === null) {
|
|
@@ -209,6 +215,13 @@ class ClusterNode {
|
|
|
209
215
|
}
|
|
210
216
|
return this._nodeId;
|
|
211
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether the handshake has set this node's id yet: the non-throwing check
|
|
220
|
+
* for the paths network input reaches before it has (#3031).
|
|
221
|
+
*/
|
|
222
|
+
get hasNodeId() {
|
|
223
|
+
return this._nodeId !== null;
|
|
224
|
+
}
|
|
212
225
|
set nodeId(id) {
|
|
213
226
|
this._nodeId = id;
|
|
214
227
|
}
|
|
@@ -389,8 +402,11 @@ class ClusterNode {
|
|
|
389
402
|
this.remoteNodes.delete(nodeId);
|
|
390
403
|
this.fullState.removeNode(nodeId);
|
|
391
404
|
subscriber.dispose();
|
|
392
|
-
|
|
393
|
-
|
|
405
|
+
// A subscriber can evict its remote node (heartbeat lost, gap, invalid
|
|
406
|
+
// message) before this node's handshake: there is no evictor id to
|
|
407
|
+
// broadcast then, and the eviction stays local (#3031).
|
|
408
|
+
if (broadcast && this._nodeId !== null) {
|
|
409
|
+
this.publisher.sendNodeEvicted(this._nodeId, nodeId, reason);
|
|
394
410
|
}
|
|
395
411
|
if (this.countActiveNodes() < this.config.minimumNodes) {
|
|
396
412
|
global.kuzzle.state = kuzzleStateEnum_1.default.NOT_ENOUGH_NODES;
|
|
@@ -15,6 +15,8 @@ type SubscribingNode = {
|
|
|
15
15
|
config: IKuzzleConfiguration["cluster"];
|
|
16
16
|
heartbeatDelay: number;
|
|
17
17
|
nodeId: string;
|
|
18
|
+
/** Whether `nodeId` can be read: false until the handshake sets it. */
|
|
19
|
+
hasNodeId: boolean;
|
|
18
20
|
fullState: State;
|
|
19
21
|
eventEmitter: {
|
|
20
22
|
emit(event: string, payload: unknown): void;
|
|
@@ -383,6 +383,12 @@ class ClusterSubscriber {
|
|
|
383
383
|
* @param message - decoded NodeEvicted protobuf message
|
|
384
384
|
*/
|
|
385
385
|
async handleNodeEviction(message) {
|
|
386
|
+
// Before the handshake this node has no id to compare, and is not part of
|
|
387
|
+
// the cluster the eviction is about: the message is ignored (#3031).
|
|
388
|
+
if (!this.localNode.hasNodeId) {
|
|
389
|
+
debug("[CLUSTER] Eviction of node %s by %s ignored: the handshake has not run", message.nodeId, message.evictor);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
386
392
|
if (message.nodeId === this.localNode.nodeId) {
|
|
387
393
|
this.logger.error(`[CLUSTER] Node evicted by ${message.evictor}. Reason: ${message.reason}`);
|
|
388
394
|
// A failure, like `evictSelf`'s: exit non-zero (#2785).
|
package/dist/lib/config/index.js
CHANGED
|
@@ -28,21 +28,27 @@ const assert_1 = __importDefault(require("assert"));
|
|
|
28
28
|
const node_util_1 = require("node:util");
|
|
29
29
|
const rc_1 = __importDefault(require("rc"));
|
|
30
30
|
const default_config_1 = __importDefault(require("./default.config"));
|
|
31
|
+
const authTokenSettings_1 = require("../core/security/authTokenSettings");
|
|
31
32
|
const package_json_1 = __importDefault(require("../../package.json"));
|
|
32
33
|
const kerror_1 = require("../kerror");
|
|
33
34
|
const safeObject_1 = require("../util/safeObject");
|
|
34
35
|
const bytes_1 = __importDefault(require("../util/bytes"));
|
|
35
36
|
const wrapped = (0, kerror_1.wrap)("core", "configuration");
|
|
36
37
|
function loadConfig() {
|
|
37
|
-
let
|
|
38
|
+
let raw;
|
|
38
39
|
try {
|
|
39
|
-
|
|
40
|
+
raw = (0, rc_1.default)("kuzzle", withoutTokenDefaults(default_config_1.default));
|
|
40
41
|
}
|
|
41
42
|
catch (e) {
|
|
42
43
|
throw wrapped.get("cannot_parse", e instanceof Error ? e.message : (0, node_util_1.inspect)(e));
|
|
43
44
|
}
|
|
44
45
|
// In place: what `unstringify` answers is the object it was given.
|
|
45
|
-
unstringify(
|
|
46
|
+
unstringify(raw);
|
|
47
|
+
const { authToken, deprecatedSettings, jwt } = resolveTokenSettings(raw.security);
|
|
48
|
+
const loaded = {
|
|
49
|
+
...raw,
|
|
50
|
+
security: { ...raw.security, authToken, jwt },
|
|
51
|
+
};
|
|
46
52
|
checkLimitsConfig(loaded);
|
|
47
53
|
checkHttpOptions(loaded);
|
|
48
54
|
checkWebSocketOptions(loaded);
|
|
@@ -56,6 +62,7 @@ function loadConfig() {
|
|
|
56
62
|
hash: {
|
|
57
63
|
seed: Buffer.from("^m&mOISKBvb1xpl1mRsrylaQXpjb&IJX"),
|
|
58
64
|
},
|
|
65
|
+
deprecatedSettings,
|
|
59
66
|
notifiableProtocols: [],
|
|
60
67
|
},
|
|
61
68
|
version: package_json_1.default.version,
|
|
@@ -66,6 +73,56 @@ function loadConfig() {
|
|
|
66
73
|
preprocessRedisOptions(config.services.memoryStorage);
|
|
67
74
|
return config;
|
|
68
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The packaged defaults, minus the values of `security.authToken` and of its
|
|
78
|
+
* deprecated spelling `security.jwt`: both sections ship the same non-null
|
|
79
|
+
* defaults, so once `rc` has merged them nothing tells a user's
|
|
80
|
+
* `jwt.expiresIn` from the packaged one. Left empty here, they hold only what
|
|
81
|
+
* the user set, and {@link resolveTokenSettings} applies the defaults after.
|
|
82
|
+
*
|
|
83
|
+
* A shallow copy: the other sections stay the packaged objects `rc` merges
|
|
84
|
+
* into, as before.
|
|
85
|
+
*/
|
|
86
|
+
function withoutTokenDefaults(defaults) {
|
|
87
|
+
const authToken = {};
|
|
88
|
+
const jwt = {};
|
|
89
|
+
return { ...defaults, security: { ...defaults.security, authToken, jwt } };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolves the authentication token settings: for each one, the value set
|
|
93
|
+
* under `security.authToken` wins, then the one set under the deprecated
|
|
94
|
+
* `security.jwt`, then the packaged default. A `null` counts as unset, as the
|
|
95
|
+
* readers' `authToken.x ?? jwt.x` always did. Exception: `gracePeriod` set in
|
|
96
|
+
* both sections keeps the `jwt` one, as it always did (see
|
|
97
|
+
* {@link LEGACY_FIRST}).
|
|
98
|
+
*
|
|
99
|
+
* Both sections get the resolved values, so a reader of either — a plugin
|
|
100
|
+
* may still read `jwt` — sees the settings in effect. `deprecatedSettings`
|
|
101
|
+
* lists the `security.jwt.*` keys the user set, for the deprecation warning.
|
|
102
|
+
*/
|
|
103
|
+
function resolveTokenSettings(security) {
|
|
104
|
+
const packaged = default_config_1.default.security.authToken;
|
|
105
|
+
const fromPackage = packaged;
|
|
106
|
+
const fromAuthToken = (0, safeObject_1.isPlainObject)(security.authToken)
|
|
107
|
+
? security.authToken
|
|
108
|
+
: {};
|
|
109
|
+
const fromJwt = (0, safeObject_1.isPlainObject)(security.jwt) ? security.jwt : {};
|
|
110
|
+
const resolved = {};
|
|
111
|
+
for (const key of new Set([
|
|
112
|
+
...Object.keys(packaged),
|
|
113
|
+
...Object.keys(fromJwt),
|
|
114
|
+
...Object.keys(fromAuthToken),
|
|
115
|
+
])) {
|
|
116
|
+
resolved[key] = authTokenSettings_1.LEGACY_FIRST.has(key)
|
|
117
|
+
? (fromJwt[key] ?? fromAuthToken[key] ?? fromPackage[key])
|
|
118
|
+
: (fromAuthToken[key] ?? fromJwt[key] ?? fromPackage[key]);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
authToken: { ...packaged, ...resolved },
|
|
122
|
+
deprecatedSettings: Object.keys(fromJwt).map((key) => `security.jwt.${key}`),
|
|
123
|
+
jwt: { ...resolved },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
69
126
|
/**
|
|
70
127
|
* RC params can be overriden using environment variables,
|
|
71
128
|
* in which case all values are passed as strings.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JSONObject } from "../../types/JSONObject";
|
|
2
2
|
import { EmbeddedSDK } from "../shared/sdk/embeddedSdk";
|
|
3
|
-
import type { EventDefinition } from "
|
|
3
|
+
import type { EventDefinition } from "../../types/EventHandler";
|
|
4
4
|
import { BackendCluster, BackendConfig, BackendController, BackendHook, BackendImport, BackendPipe, BackendPlugin, BackendStorage, BackendVault, BackendOpenApi, BackendErrors, BackendSubscription } from "./index";
|
|
5
5
|
import { Logger } from "../../kuzzle/Logger";
|
|
6
6
|
import Plugin from "../plugin/plugin";
|
|
@@ -99,13 +99,14 @@ export declare class Backend {
|
|
|
99
99
|
*/
|
|
100
100
|
plugin: BackendPlugin;
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
102
|
+
* Application logger
|
|
103
103
|
*
|
|
104
|
+
* @method trace
|
|
104
105
|
* @method debug
|
|
105
106
|
* @method info
|
|
106
107
|
* @method warn
|
|
107
108
|
* @method error
|
|
108
|
-
* @method
|
|
109
|
+
* @method fatal
|
|
109
110
|
*/
|
|
110
111
|
log: Logger;
|
|
111
112
|
/**
|