@blamejs/core 0.4.28 → 0.5.0
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/CHANGELOG.md +2 -0
- package/index.js +2 -0
- package/lib/api-key.js +4 -10
- package/lib/audit-tools.js +9 -7
- package/lib/audit.js +1 -0
- package/lib/auth/oauth.js +11 -4
- package/lib/backup/manifest.js +4 -1
- package/lib/break-glass.js +741 -0
- package/lib/cache.js +1 -9
- package/lib/db.js +64 -0
- package/lib/framework-schema.js +71 -0
- package/lib/mail.js +13 -2
- package/lib/middleware/api-encrypt.js +24 -7
- package/lib/middleware/attach-user.js +3 -2
- package/lib/middleware/cors.js +11 -1
- package/lib/notify.js +1 -9
- package/lib/request-helpers.js +30 -4
- package/lib/restore-rollback.js +3 -1
- package/lib/seeders.js +1 -9
- package/lib/vault/rotate.js +5 -1
- package/package.json +1 -1
package/lib/cache.js
CHANGED
|
@@ -683,15 +683,7 @@ function create(opts) {
|
|
|
683
683
|
}
|
|
684
684
|
|
|
685
685
|
function _actor(callerOpts) {
|
|
686
|
-
|
|
687
|
-
if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
|
|
688
|
-
for (var k in callerOpts.context) {
|
|
689
|
-
if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
|
|
690
|
-
override[k] = callerOpts.context[k];
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
return requestHelpers.extractActorContext(callerOpts && callerOpts.req, override);
|
|
686
|
+
return requestHelpers.resolveActorWithOverride(callerOpts);
|
|
695
687
|
}
|
|
696
688
|
|
|
697
689
|
function _backendFailedAudit(op, err) {
|
package/lib/db.js
CHANGED
|
@@ -109,6 +109,8 @@ var RESERVED_TABLE_NAMES = new Set([
|
|
|
109
109
|
"_blamejs_cache",
|
|
110
110
|
"_blamejs_seeders",
|
|
111
111
|
"_blamejs_seeders_lock",
|
|
112
|
+
"_blamejs_break_glass_policies",
|
|
113
|
+
"_blamejs_break_glass_grants",
|
|
112
114
|
]);
|
|
113
115
|
|
|
114
116
|
var FRAMEWORK_SCHEMA = [
|
|
@@ -423,6 +425,67 @@ var FRAMEWORK_SCHEMA = [
|
|
|
423
425
|
},
|
|
424
426
|
sealedFields: [],
|
|
425
427
|
},
|
|
428
|
+
{
|
|
429
|
+
// _blamejs_break_glass_policies — column-level break-glass policy
|
|
430
|
+
// registry. One row per (table) declares which columns are
|
|
431
|
+
// glass-locked and what the operator's grant rules are. Sealed
|
|
432
|
+
// columns hold the column-list, factor-list, and bypass config so
|
|
433
|
+
// policy contents aren't browsable in cleartext.
|
|
434
|
+
name: "_blamejs_break_glass_policies",
|
|
435
|
+
columns: {
|
|
436
|
+
tableName: "TEXT PRIMARY KEY",
|
|
437
|
+
columnsJson: "TEXT NOT NULL",
|
|
438
|
+
factorsJson: "TEXT NOT NULL",
|
|
439
|
+
cryptographic: "INTEGER NOT NULL DEFAULT 0",
|
|
440
|
+
grantTtlMs: "INTEGER NOT NULL",
|
|
441
|
+
maxRowsPerGrant: "INTEGER NOT NULL DEFAULT 1",
|
|
442
|
+
reasonRequired: "INTEGER NOT NULL DEFAULT 1",
|
|
443
|
+
reasonMinLength: "INTEGER NOT NULL DEFAULT 12",
|
|
444
|
+
pinIp: "INTEGER NOT NULL DEFAULT 1",
|
|
445
|
+
sessionPin: "INTEGER NOT NULL DEFAULT 1",
|
|
446
|
+
onLockedAccess: "TEXT NOT NULL DEFAULT 'throw'",
|
|
447
|
+
requireScope: "TEXT",
|
|
448
|
+
serviceAccountBypassJson: "TEXT",
|
|
449
|
+
auditReasonStorage: "TEXT NOT NULL DEFAULT 'cleartext'",
|
|
450
|
+
updatedAt: "INTEGER NOT NULL",
|
|
451
|
+
},
|
|
452
|
+
indexes: [],
|
|
453
|
+
sealedFields: ["columnsJson", "factorsJson", "serviceAccountBypassJson"],
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
// _blamejs_break_glass_grants — issued grants. Each successful
|
|
457
|
+
// step-up creates one row; each row read decrements rowsRemaining.
|
|
458
|
+
// Default maxRowsPerGrant=1 enforces "row by row" auth per the
|
|
459
|
+
// operator-confirmed shape (each row access = its own grant).
|
|
460
|
+
// Sealed columns hold reason + scopeColumns so audit-readable
|
|
461
|
+
// metadata doesn't leak in cleartext.
|
|
462
|
+
name: "_blamejs_break_glass_grants",
|
|
463
|
+
columns: {
|
|
464
|
+
_id: "TEXT PRIMARY KEY",
|
|
465
|
+
issuedToActorId: "TEXT NOT NULL",
|
|
466
|
+
issuedToActorHash: "TEXT NOT NULL",
|
|
467
|
+
factorType: "TEXT NOT NULL",
|
|
468
|
+
reasonSealed: "TEXT",
|
|
469
|
+
scopeTable: "TEXT NOT NULL",
|
|
470
|
+
scopeColumnsJson: "TEXT NOT NULL",
|
|
471
|
+
issuedAt: "INTEGER NOT NULL",
|
|
472
|
+
expiresAt: "INTEGER NOT NULL",
|
|
473
|
+
maxRowsPerGrant: "INTEGER NOT NULL",
|
|
474
|
+
rowsConsumed: "INTEGER NOT NULL DEFAULT 0",
|
|
475
|
+
revokedAt: "INTEGER",
|
|
476
|
+
sessionId: "TEXT",
|
|
477
|
+
ip: "TEXT",
|
|
478
|
+
kwGrantHalf: "TEXT",
|
|
479
|
+
},
|
|
480
|
+
indexes: [
|
|
481
|
+
{ name: "idx_bg_grants_actor", columns: ["issuedToActorHash"] },
|
|
482
|
+
{ name: "idx_bg_grants_table", columns: ["scopeTable"] },
|
|
483
|
+
"expiresAt",
|
|
484
|
+
"revokedAt",
|
|
485
|
+
],
|
|
486
|
+
derivedHashes: { issuedToActorHash: { from: "issuedToActorId" } },
|
|
487
|
+
sealedFields: ["reasonSealed", "scopeColumnsJson"],
|
|
488
|
+
},
|
|
426
489
|
];
|
|
427
490
|
|
|
428
491
|
var log = boot("db");
|
|
@@ -1118,6 +1181,7 @@ module.exports = {
|
|
|
1118
1181
|
try { require("./storage")._resetForTest(); } catch (_e) {}
|
|
1119
1182
|
try { require("./audit-sign")._resetForTest(); } catch (_e) {}
|
|
1120
1183
|
try { require("./queue")._resetForTest(); } catch (_e) {}
|
|
1184
|
+
try { require("./break-glass")._resetForTest(); } catch (_e) {}
|
|
1121
1185
|
try { require("./log-stream")._resetForTest(); } catch (_e) {}
|
|
1122
1186
|
try { require("./redact")._resetForTest(); } catch (_e) {}
|
|
1123
1187
|
try { require("./external-db")._resetForTest(); } catch (_e) {}
|
package/lib/framework-schema.js
CHANGED
|
@@ -120,6 +120,11 @@ var LOCAL_TO_EXTERNAL = Object.freeze({
|
|
|
120
120
|
// FRAMEWORK_SCHEMA so cluster-storage.execute routes to either side.
|
|
121
121
|
_blamejs_seeders: "_blamejs_seeders",
|
|
122
122
|
_blamejs_seeders_lock: "_blamejs_seeders_lock",
|
|
123
|
+
// Break-glass policy + grant tables. Cluster-shared so a grant
|
|
124
|
+
// issued on node A is honored on node B; policies updated on the
|
|
125
|
+
// leader propagate to all followers via the shared table.
|
|
126
|
+
_blamejs_break_glass_policies: "_blamejs_break_glass_policies",
|
|
127
|
+
_blamejs_break_glass_grants: "_blamejs_break_glass_grants",
|
|
123
128
|
});
|
|
124
129
|
|
|
125
130
|
function tableName(localName) {
|
|
@@ -556,6 +561,70 @@ function _cacheDDL(dialect) {
|
|
|
556
561
|
};
|
|
557
562
|
}
|
|
558
563
|
|
|
564
|
+
// _blamejs_break_glass_policies — column-level break-glass policy
|
|
565
|
+
// registry. One row per (table) declares which columns are
|
|
566
|
+
// glass-locked + the operator's grant rules. Sealed columns hide
|
|
567
|
+
// column-list / factor-list / bypass config from cleartext browsing.
|
|
568
|
+
function _breakGlassPoliciesDDL(dialect) {
|
|
569
|
+
var t = _types(dialect);
|
|
570
|
+
var name = LOCAL_TO_EXTERNAL._blamejs_break_glass_policies;
|
|
571
|
+
return {
|
|
572
|
+
create:
|
|
573
|
+
"CREATE TABLE IF NOT EXISTS " + name + " (" +
|
|
574
|
+
" tableName TEXT PRIMARY KEY," +
|
|
575
|
+
" columnsJson TEXT NOT NULL," +
|
|
576
|
+
" factorsJson TEXT NOT NULL," +
|
|
577
|
+
" cryptographic " + t.INT + " NOT NULL DEFAULT 0," +
|
|
578
|
+
" grantTtlMs " + t.INT + " NOT NULL," +
|
|
579
|
+
" maxRowsPerGrant " + t.INT + " NOT NULL DEFAULT 1," +
|
|
580
|
+
" reasonRequired " + t.INT + " NOT NULL DEFAULT 1," +
|
|
581
|
+
" reasonMinLength " + t.INT + " NOT NULL DEFAULT 12," +
|
|
582
|
+
" pinIp " + t.INT + " NOT NULL DEFAULT 1," +
|
|
583
|
+
" sessionPin " + t.INT + " NOT NULL DEFAULT 1," +
|
|
584
|
+
" onLockedAccess TEXT NOT NULL DEFAULT 'throw'," +
|
|
585
|
+
" requireScope TEXT," +
|
|
586
|
+
" serviceAccountBypassJson TEXT," +
|
|
587
|
+
" auditReasonStorage TEXT NOT NULL DEFAULT 'cleartext'," +
|
|
588
|
+
" updatedAt " + t.INT + " NOT NULL" +
|
|
589
|
+
")",
|
|
590
|
+
indexes: [],
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// _blamejs_break_glass_grants — issued grants. One row per successful
|
|
595
|
+
// step-up. Default maxRowsPerGrant=1 enforces row-by-row auth per the
|
|
596
|
+
// operator-confirmed shape ("each row access = its own grant").
|
|
597
|
+
function _breakGlassGrantsDDL(dialect) {
|
|
598
|
+
var t = _types(dialect);
|
|
599
|
+
var name = LOCAL_TO_EXTERNAL._blamejs_break_glass_grants;
|
|
600
|
+
return {
|
|
601
|
+
create:
|
|
602
|
+
"CREATE TABLE IF NOT EXISTS " + name + " (" +
|
|
603
|
+
" _id TEXT PRIMARY KEY," +
|
|
604
|
+
" issuedToActorId TEXT NOT NULL," +
|
|
605
|
+
" issuedToActorHash TEXT NOT NULL," +
|
|
606
|
+
" factorType TEXT NOT NULL," +
|
|
607
|
+
" reasonSealed TEXT," +
|
|
608
|
+
" scopeTable TEXT NOT NULL," +
|
|
609
|
+
" scopeColumnsJson TEXT NOT NULL," +
|
|
610
|
+
" issuedAt " + t.INT + " NOT NULL," +
|
|
611
|
+
" expiresAt " + t.INT + " NOT NULL," +
|
|
612
|
+
" maxRowsPerGrant " + t.INT + " NOT NULL," +
|
|
613
|
+
" rowsConsumed " + t.INT + " NOT NULL DEFAULT 0," +
|
|
614
|
+
" revokedAt " + t.INT + "," +
|
|
615
|
+
" sessionId TEXT," +
|
|
616
|
+
" ip TEXT," +
|
|
617
|
+
" kwGrantHalf TEXT" +
|
|
618
|
+
")",
|
|
619
|
+
indexes: [
|
|
620
|
+
"CREATE INDEX IF NOT EXISTS idx_" + name + "_actor ON " + name + " (issuedToActorHash)",
|
|
621
|
+
"CREATE INDEX IF NOT EXISTS idx_" + name + "_table ON " + name + " (scopeTable)",
|
|
622
|
+
"CREATE INDEX IF NOT EXISTS idx_" + name + "_expires ON " + name + " (expiresAt)",
|
|
623
|
+
"CREATE INDEX IF NOT EXISTS idx_" + name + "_revoked ON " + name + " (revokedAt)",
|
|
624
|
+
],
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
559
628
|
// ---- ensureSchema ----
|
|
560
629
|
|
|
561
630
|
async function ensureSchema(opts) {
|
|
@@ -590,6 +659,8 @@ async function ensureSchema(opts) {
|
|
|
590
659
|
_cacheDDL(dialect),
|
|
591
660
|
_seedersDDL(dialect),
|
|
592
661
|
_seedersLockDDL(dialect),
|
|
662
|
+
_breakGlassPoliciesDDL(dialect),
|
|
663
|
+
_breakGlassGrantsDDL(dialect),
|
|
593
664
|
];
|
|
594
665
|
|
|
595
666
|
var created = [];
|
package/lib/mail.js
CHANGED
|
@@ -62,15 +62,22 @@
|
|
|
62
62
|
* resend-*. Vendor-specific presets carry their own code prefix so
|
|
63
63
|
* diagnostic logs identify the provider that rejected the message.
|
|
64
64
|
*/
|
|
65
|
+
var C = require("./constants");
|
|
65
66
|
var lazyRequire = require("./lazy-require");
|
|
67
|
+
var nodeCrypto = require("node:crypto");
|
|
66
68
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
67
69
|
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
68
70
|
var net = lazyRequire(function () { return require("net"); });
|
|
69
71
|
var tls = lazyRequire(function () { return require("tls"); });
|
|
72
|
+
var safeJson = require("./safe-json");
|
|
70
73
|
var safeSchema = require("./safe-schema");
|
|
71
74
|
var validateOpts = require("./validate-opts");
|
|
72
75
|
var { FrameworkError } = require("./framework-error");
|
|
73
76
|
|
|
77
|
+
// Cap on responses parsed from upstream mail providers (Resend, etc.).
|
|
78
|
+
// Vendor responses are tiny in spec; 256 KiB is generous headroom.
|
|
79
|
+
var MAIL_RESPONSE_MAX_BYTES = C.BYTES.kib(256);
|
|
80
|
+
|
|
74
81
|
class MailError extends FrameworkError {
|
|
75
82
|
constructor(code, message, permanent, statusCode) {
|
|
76
83
|
super(message, code);
|
|
@@ -286,7 +293,11 @@ function memoryTransport() {
|
|
|
286
293
|
// to send AUTH or DATA in cleartext if the upgrade is rejected.
|
|
287
294
|
|
|
288
295
|
function _newBoundary(label) {
|
|
289
|
-
|
|
296
|
+
// crypto.randomBytes for the boundary suffix matches the framework
|
|
297
|
+
// convention. RFC 5322 only requires uniqueness within a message,
|
|
298
|
+
// but consistency with how every other identifier in lib/ is built
|
|
299
|
+
// wins over premature differentiation.
|
|
300
|
+
return "blamejs-" + label + "-" + Date.now() + "-" + nodeCrypto.randomBytes(8).toString("hex");
|
|
290
301
|
}
|
|
291
302
|
|
|
292
303
|
// base64-encode the buffer with line wrapping at 76 chars (RFC 2045
|
|
@@ -773,7 +784,7 @@ function resendTransport(opts) {
|
|
|
773
784
|
interpret: function (res) {
|
|
774
785
|
var text = res.body ? res.body.toString("utf8") : "";
|
|
775
786
|
var data;
|
|
776
|
-
try { data =
|
|
787
|
+
try { data = safeJson.parse(text, { maxBytes: MAIL_RESPONSE_MAX_BYTES }); }
|
|
777
788
|
catch (_e) {
|
|
778
789
|
throw new MailError("mail/resend-bad-response",
|
|
779
790
|
"resend response was not JSON: " + text.slice(0, 200), false);
|
|
@@ -100,6 +100,8 @@ var crypto = require("../crypto");
|
|
|
100
100
|
var C = require("../constants");
|
|
101
101
|
var lazyRequire = require("../lazy-require");
|
|
102
102
|
var nonceStoreLib = require("../nonce-store");
|
|
103
|
+
var requestHelpers = require("../request-helpers");
|
|
104
|
+
var safeJson = require("../safe-json");
|
|
103
105
|
var validateOpts = require("../validate-opts");
|
|
104
106
|
var { defineClass } = require("../framework-error");
|
|
105
107
|
|
|
@@ -172,10 +174,19 @@ function create(opts) {
|
|
|
172
174
|
validateOpts(opts, [
|
|
173
175
|
"keypair", "keypairs", "replayWindowMs", "pruneIntervalMs",
|
|
174
176
|
"nonceStore", "exemptPaths", "contentTypes", "audit",
|
|
177
|
+
"maxDecryptedBytes",
|
|
175
178
|
], "middleware.apiEncrypt");
|
|
176
179
|
var keypairs = _resolveKeypairs(opts);
|
|
177
180
|
var activeKeypair = keypairs[0];
|
|
178
181
|
var replayWindowMs = opts.replayWindowMs || DEFAULT_REPLAY_WINDOW_MS;
|
|
182
|
+
// Cap on decrypted-payload size handed to safeJson.parse. Defaults
|
|
183
|
+
// to 4 MiB (bodyParser's default 1 MiB plus headroom for crypto +
|
|
184
|
+
// base64 round-trip). Operators with chunkier inbound payloads
|
|
185
|
+
// raise this; the framework refuses to parse anything larger as a
|
|
186
|
+
// parse-bomb defense.
|
|
187
|
+
var maxDecryptedBytes = opts.maxDecryptedBytes != null
|
|
188
|
+
? opts.maxDecryptedBytes
|
|
189
|
+
: C.BYTES.mib(4);
|
|
179
190
|
// The spec calls for a sweep cadence of replayWindowMs/2 — short
|
|
180
191
|
// enough that expired nonces don't pile up but not so frequent the
|
|
181
192
|
// sweep query becomes a hot path. Operators can override.
|
|
@@ -230,7 +241,7 @@ function create(opts) {
|
|
|
230
241
|
};
|
|
231
242
|
if (auditOn) {
|
|
232
243
|
audit().safeEmit({
|
|
233
|
-
actor:
|
|
244
|
+
actor: requestHelpers.extractActorContext(req),
|
|
234
245
|
action: "system.api_encrypt.failure",
|
|
235
246
|
outcome: "denied",
|
|
236
247
|
reason: reason,
|
|
@@ -348,7 +359,7 @@ function create(opts) {
|
|
|
348
359
|
try {
|
|
349
360
|
var ctBuf = Buffer.from(ct, "base64");
|
|
350
361
|
var ptBuf = crypto.decryptPacked(ctBuf, sessionKey);
|
|
351
|
-
clearObj =
|
|
362
|
+
clearObj = safeJson.parse(ptBuf.toString("utf8"), { maxBytes: maxDecryptedBytes });
|
|
352
363
|
} catch (_e) {
|
|
353
364
|
_emitFailure(req, "tag");
|
|
354
365
|
return _writeRejection(res, 400, { error: "encrypted-payload-rejected" });
|
|
@@ -403,7 +414,7 @@ function create(opts) {
|
|
|
403
414
|
|
|
404
415
|
function client(opts) {
|
|
405
416
|
opts = opts || {};
|
|
406
|
-
validateOpts(opts, ["pubkey"], "middleware.apiEncrypt.client");
|
|
417
|
+
validateOpts(opts, ["pubkey", "maxDecryptedBytes"], "middleware.apiEncrypt.client");
|
|
407
418
|
if (!opts.pubkey || typeof opts.pubkey !== "object") {
|
|
408
419
|
throw _err("CLIENT_INVALID_PUBKEY",
|
|
409
420
|
"apiEncrypt.client: opts.pubkey is required ({ publicKey, ecPublicKey })", 500);
|
|
@@ -414,6 +425,9 @@ function client(opts) {
|
|
|
414
425
|
"apiEncrypt.client: pubkey.publicKey + ecPublicKey must be PEM strings", 500);
|
|
415
426
|
}
|
|
416
427
|
var pubkey = opts.pubkey;
|
|
428
|
+
var maxDecryptedBytes = opts.maxDecryptedBytes != null
|
|
429
|
+
? opts.maxDecryptedBytes
|
|
430
|
+
: C.BYTES.mib(4);
|
|
417
431
|
|
|
418
432
|
function encryptRequest(payload) {
|
|
419
433
|
if (payload === undefined) payload = null;
|
|
@@ -441,7 +455,7 @@ function client(opts) {
|
|
|
441
455
|
}
|
|
442
456
|
var resCtBuf = Buffer.from(responseBody._ct, "base64");
|
|
443
457
|
var resPtBuf = crypto.decryptPacked(resCtBuf, sessionKey);
|
|
444
|
-
return
|
|
458
|
+
return safeJson.parse(resPtBuf.toString("utf8"), { maxBytes: maxDecryptedBytes });
|
|
445
459
|
},
|
|
446
460
|
};
|
|
447
461
|
}
|
|
@@ -474,13 +488,16 @@ function client(opts) {
|
|
|
474
488
|
function httpClientEncrypted(opts) {
|
|
475
489
|
opts = opts || {};
|
|
476
490
|
validateOpts(opts, [
|
|
477
|
-
"pubkey", "baseUrl", "headers", "method",
|
|
491
|
+
"pubkey", "baseUrl", "headers", "method", "maxDecryptedBytes",
|
|
478
492
|
], "middleware.apiEncrypt.httpClient");
|
|
479
493
|
if (!opts.pubkey) {
|
|
480
494
|
throw _err("CLIENT_INVALID_PUBKEY",
|
|
481
495
|
"httpClient.encrypted: opts.pubkey is required (the callee's bootstrap doc)", 500);
|
|
482
496
|
}
|
|
483
|
-
var
|
|
497
|
+
var maxDecryptedBytes = opts.maxDecryptedBytes != null
|
|
498
|
+
? opts.maxDecryptedBytes
|
|
499
|
+
: C.BYTES.mib(4);
|
|
500
|
+
var clientCtx = client({ pubkey: opts.pubkey, maxDecryptedBytes: maxDecryptedBytes });
|
|
484
501
|
var baseUrl = opts.baseUrl ? String(opts.baseUrl).replace(/\/$/, "") : "";
|
|
485
502
|
var defaultHdrs = opts.headers || {};
|
|
486
503
|
var defaultMethod = opts.method || "POST";
|
|
@@ -531,7 +548,7 @@ function httpClientEncrypted(opts) {
|
|
|
531
548
|
return { statusCode: resp.statusCode, headers: resp.headers, body: null };
|
|
532
549
|
}
|
|
533
550
|
var parsed;
|
|
534
|
-
try { parsed =
|
|
551
|
+
try { parsed = safeJson.parse(resp.body.toString("utf8"), { maxBytes: maxDecryptedBytes }); }
|
|
535
552
|
catch (e) {
|
|
536
553
|
throw _err("CLIENT_RESPONSE_NOT_JSON",
|
|
537
554
|
"httpClient.encrypted: response body is not valid JSON: " + e.message);
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
var lazyRequire = require("../lazy-require");
|
|
45
45
|
var cookies = require("../cookies");
|
|
46
|
+
var requestHelpers = require("../request-helpers");
|
|
46
47
|
var validateOpts = require("../validate-opts");
|
|
47
48
|
var session = lazyRequire(function () { return require("../session"); });
|
|
48
49
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
@@ -120,7 +121,7 @@ function create(opts) {
|
|
|
120
121
|
audit().emit({
|
|
121
122
|
action: "auth.session.user_loader_threw",
|
|
122
123
|
outcome: "failure",
|
|
123
|
-
actor: { userId: verified.userId
|
|
124
|
+
actor: requestHelpers.extractActorContext(req, { userId: verified.userId }),
|
|
124
125
|
reason: (e && e.message) || String(e),
|
|
125
126
|
});
|
|
126
127
|
} catch (_ignored) { /* audit best-effort */ }
|
|
@@ -135,7 +136,7 @@ function create(opts) {
|
|
|
135
136
|
audit().emit({
|
|
136
137
|
action: "auth.session.user_unloadable",
|
|
137
138
|
outcome: "failure",
|
|
138
|
-
actor: { userId: verified.userId
|
|
139
|
+
actor: requestHelpers.extractActorContext(req, { userId: verified.userId }),
|
|
139
140
|
});
|
|
140
141
|
} catch (_ignored) { /* audit best-effort */ }
|
|
141
142
|
}
|
package/lib/middleware/cors.js
CHANGED
|
@@ -44,10 +44,20 @@
|
|
|
44
44
|
*/
|
|
45
45
|
var lazyRequire = require("../lazy-require");
|
|
46
46
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
47
|
+
var requestHelpers = require("../request-helpers");
|
|
47
48
|
var safeUrl = require("../safe-url");
|
|
48
49
|
var validateOpts = require("../validate-opts");
|
|
49
50
|
var { defineClass } = require("../framework-error");
|
|
50
51
|
|
|
52
|
+
// CORS audit events trust the proxy header for actor.ip — same
|
|
53
|
+
// semantics as bot-guard (the apparent origin is what matters when
|
|
54
|
+
// blocking cross-origin attempts behind a CDN).
|
|
55
|
+
function _xffIp(req) {
|
|
56
|
+
var xff = req.headers && req.headers["x-forwarded-for"];
|
|
57
|
+
if (xff) return String(xff).split(",")[0].trim();
|
|
58
|
+
return (req.socket && req.socket.remoteAddress) || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
var CorsError = defineClass("CorsError", { alwaysPermanent: true });
|
|
52
62
|
|
|
53
63
|
function _matchOrigin(origin, allowList) {
|
|
@@ -188,7 +198,7 @@ function create(opts) {
|
|
|
188
198
|
if (refuseUnknown) {
|
|
189
199
|
try {
|
|
190
200
|
audit().emit({
|
|
191
|
-
actor: { ip: (req
|
|
201
|
+
actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
|
|
192
202
|
action: "system.cors.block",
|
|
193
203
|
outcome: "denied",
|
|
194
204
|
reason: "origin not in allow-list",
|
package/lib/notify.js
CHANGED
|
@@ -370,15 +370,7 @@ function create(opts) {
|
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
function _actor(callerOpts) {
|
|
373
|
-
|
|
374
|
-
if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
|
|
375
|
-
for (var k in callerOpts.context) {
|
|
376
|
-
if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
|
|
377
|
-
override[k] = callerOpts.context[k];
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return requestHelpers.extractActorContext(callerOpts && callerOpts.req, override);
|
|
373
|
+
return requestHelpers.resolveActorWithOverride(callerOpts);
|
|
382
374
|
}
|
|
383
375
|
|
|
384
376
|
function _redactedMetadata(channel, message, extra) {
|
package/lib/request-helpers.js
CHANGED
|
@@ -87,6 +87,31 @@ function extractActorContext(req, override) {
|
|
|
87
87
|
return ctx;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Convenience wrapper for primitives that accept an optional
|
|
91
|
+
// `{ req, context }` shape and want to thread it into an audit-emit
|
|
92
|
+
// `actor` field. Replaces the four near-identical `_actor()` helpers
|
|
93
|
+
// that lived in api-key, cache, seeders, and notify before v0.4.29.
|
|
94
|
+
//
|
|
95
|
+
// callerOpts: operator-supplied `{ req?, context? }` (e.g. the
|
|
96
|
+
// primitive's call-site opts bag)
|
|
97
|
+
// baseOverride: optional seed values applied BEFORE callerOpts.context
|
|
98
|
+
// so `context` wins. api-key seeds `{ userId }` here so
|
|
99
|
+
// the resolved key's owner becomes the default actor
|
|
100
|
+
// unless the operator passes their own context.userId.
|
|
101
|
+
//
|
|
102
|
+
// Returns the same shape as extractActorContext.
|
|
103
|
+
function resolveActorWithOverride(callerOpts, baseOverride) {
|
|
104
|
+
var override = baseOverride ? Object.assign({}, baseOverride) : {};
|
|
105
|
+
if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
|
|
106
|
+
for (var k in callerOpts.context) {
|
|
107
|
+
if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
|
|
108
|
+
override[k] = callerOpts.context[k];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return extractActorContext(callerOpts && callerOpts.req, override);
|
|
113
|
+
}
|
|
114
|
+
|
|
90
115
|
function resolveRoute(req) {
|
|
91
116
|
if (req && typeof req.routePattern === "string" && req.routePattern.length > 0) {
|
|
92
117
|
return req.routePattern;
|
|
@@ -171,8 +196,9 @@ function parseQualityList(headerValue, opts) {
|
|
|
171
196
|
}
|
|
172
197
|
|
|
173
198
|
module.exports = {
|
|
174
|
-
resolveRoute:
|
|
175
|
-
captureResponseStatus:
|
|
176
|
-
extractActorContext:
|
|
177
|
-
|
|
199
|
+
resolveRoute: resolveRoute,
|
|
200
|
+
captureResponseStatus: captureResponseStatus,
|
|
201
|
+
extractActorContext: extractActorContext,
|
|
202
|
+
resolveActorWithOverride: resolveActorWithOverride,
|
|
203
|
+
parseQualityList: parseQualityList,
|
|
178
204
|
};
|
package/lib/restore-rollback.js
CHANGED
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
var fs = require("fs");
|
|
57
57
|
var path = require("path");
|
|
58
58
|
var atomicFile = require("./atomic-file");
|
|
59
|
+
var C = require("./constants");
|
|
60
|
+
var safeJson = require("./safe-json");
|
|
59
61
|
var { defineClass } = require("./framework-error");
|
|
60
62
|
|
|
61
63
|
var RestoreRollbackError = defineClass("RestoreRollbackError", { alwaysPermanent: true });
|
|
@@ -199,7 +201,7 @@ function list(opts) {
|
|
|
199
201
|
var markerPath = p + ".marker.json";
|
|
200
202
|
var marker = null;
|
|
201
203
|
if (fs.existsSync(markerPath)) {
|
|
202
|
-
try { marker =
|
|
204
|
+
try { marker = safeJson.parse(fs.readFileSync(markerPath, "utf8"), { maxBytes: C.BYTES.kib(64) }); }
|
|
203
205
|
catch (_e) { marker = null; }
|
|
204
206
|
}
|
|
205
207
|
var stat;
|
package/lib/seeders.js
CHANGED
|
@@ -404,15 +404,7 @@ function create(opts) {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
function _actor(callerOpts) {
|
|
407
|
-
|
|
408
|
-
if (callerOpts && callerOpts.context && typeof callerOpts.context === "object") {
|
|
409
|
-
for (var k in callerOpts.context) {
|
|
410
|
-
if (Object.prototype.hasOwnProperty.call(callerOpts.context, k)) {
|
|
411
|
-
override[k] = callerOpts.context[k];
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return requestHelpers.extractActorContext(callerOpts && callerOpts.req, override);
|
|
407
|
+
return requestHelpers.resolveActorWithOverride(callerOpts);
|
|
416
408
|
}
|
|
417
409
|
|
|
418
410
|
function _appliedRows(db, env) {
|
package/lib/vault/rotate.js
CHANGED
|
@@ -56,6 +56,7 @@ var C = require("../constants");
|
|
|
56
56
|
var cryptoField = require("../crypto-field");
|
|
57
57
|
var cryptoLib = require("../crypto");
|
|
58
58
|
var lazyRequire = require("../lazy-require");
|
|
59
|
+
var safeJson = require("../safe-json");
|
|
59
60
|
var vaultWrap = lazyRequire(function () { return require("./wrap"); });
|
|
60
61
|
var { defineClass } = require("../framework-error");
|
|
61
62
|
|
|
@@ -479,7 +480,10 @@ function _rotateOverflow(db, table, oldKeys, newKeys, batchSize, progress, warni
|
|
|
479
480
|
for (var i = 0; i < rows.length; i++) {
|
|
480
481
|
var row = rows[i];
|
|
481
482
|
var doc;
|
|
482
|
-
|
|
483
|
+
// Vault overflow rows are framework-written but read at rotate
|
|
484
|
+
// time — gate the parse via safeJson so a corrupted row can't
|
|
485
|
+
// stage a parse-bomb against the rotation worker.
|
|
486
|
+
try { doc = safeJson.parse(row.data, { maxBytes: C.BYTES.mib(16) }); }
|
|
483
487
|
catch (_e) {
|
|
484
488
|
warnings.push("malformed overflow JSON at " + table + "._id=" + row._id + " — left unrotated");
|
|
485
489
|
continue;
|