@astram/guard 0.15.0 → 0.16.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/dist/auth/auth.module.d.ts +8 -1
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +19 -1
- package/dist/boot/boot-sequence.d.ts +6 -0
- package/dist/boot/boot-sequence.d.ts.map +1 -1
- package/dist/boot/boot-sequence.js +34 -2
- package/dist/boot/single-layout-probe.d.ts +66 -0
- package/dist/boot/single-layout-probe.d.ts.map +1 -0
- package/dist/boot/single-layout-probe.js +334 -0
- package/dist/config/app-config.d.ts +17 -0
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +54 -6
- package/package.json +3 -3
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
* `createIdentityBroker`/`createTotpFactorVerifier`) are pure; the adapters + the
|
|
63
63
|
* node:crypto HMAC live in guard. Core stays zero-dep + unchanged.
|
|
64
64
|
*/
|
|
65
|
-
import { type DynamicModule } from '@nestjs/common';
|
|
65
|
+
import { type DynamicModule, type Provider } from '@nestjs/common';
|
|
66
66
|
import type { CredentialResolver, HmacSha1, FactorVerifier } from '@astram/core';
|
|
67
67
|
import type { SessionFactsProjector } from './ports/session-facts-projector.js';
|
|
68
68
|
import type { ScopeSubtreeResolver } from './ports/scope-subtree-resolver.js';
|
|
@@ -106,6 +106,13 @@ export declare function resolveTotpVerifier(override: FactorVerifier | null | un
|
|
|
106
106
|
* consumer's built CJS dist. Async because of {@link loadCore}.
|
|
107
107
|
*/
|
|
108
108
|
export declare function makeDefaultTotpVerifier(): Promise<FactorVerifier>;
|
|
109
|
+
/** DI token for the single-layout privilege probe hook (a no-op in the four layout). */
|
|
110
|
+
export declare const SHIELD_SINGLE_LAYOUT_PROBE: unique symbol;
|
|
111
|
+
/**
|
|
112
|
+
* Single-database-layout spec §5.F (ADR-0041): the privilege probe runs once at bootstrap on this
|
|
113
|
+
* module's runtime pool, single layout only (four layout: a no-op hook). Never blocks boot.
|
|
114
|
+
*/
|
|
115
|
+
export declare const singleLayoutProbeProvider: Provider;
|
|
109
116
|
/**
|
|
110
117
|
* Options for {@link AuthModule.forRoot} — registration-time collaborators the
|
|
111
118
|
* consumer plugs through {@link ShieldModule.forRoot}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"auth.module.d.ts","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/auth.module.js
CHANGED
|
@@ -104,7 +104,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
104
104
|
})();
|
|
105
105
|
var AuthModule_1;
|
|
106
106
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
107
|
-
exports.AuthModule = exports.hmacSha1 = void 0;
|
|
107
|
+
exports.AuthModule = exports.singleLayoutProbeProvider = exports.SHIELD_SINGLE_LAYOUT_PROBE = exports.hmacSha1 = void 0;
|
|
108
108
|
exports.resolveTotpVerifier = resolveTotpVerifier;
|
|
109
109
|
exports.makeDefaultTotpVerifier = makeDefaultTotpVerifier;
|
|
110
110
|
exports.resolveDefaultEmailSender = resolveDefaultEmailSender;
|
|
@@ -113,6 +113,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
113
113
|
const app_config_js_1 = require("../config/app-config.js");
|
|
114
114
|
const config_module_js_1 = require("../config/config.module.js");
|
|
115
115
|
const migration_ledger_check_js_1 = require("../boot/migration-ledger-check.js");
|
|
116
|
+
const single_layout_probe_js_1 = require("../boot/single-layout-probe.js");
|
|
116
117
|
const migrations_dir_js_1 = require("../migrations-dir.js");
|
|
117
118
|
const argon2id_password_hasher_js_1 = require("../authentication/argon2id-password-hasher.js");
|
|
118
119
|
const mariadb_credential_resolver_js_1 = require("../authentication/mariadb-credential-resolver.js");
|
|
@@ -387,6 +388,22 @@ const migrationLedgerCheckProvider = {
|
|
|
387
388
|
registry ?? null),
|
|
388
389
|
inject: [tokens_js_1.SHIELD_DB_POOL, config_module_js_1.APP_CONFIG, { token: migration_ledger_check_js_1.SHIELD_MIGRATION_LEDGER_REGISTRY, optional: true }],
|
|
389
390
|
};
|
|
391
|
+
/** DI token for the single-layout privilege probe hook (a no-op in the four layout). */
|
|
392
|
+
exports.SHIELD_SINGLE_LAYOUT_PROBE = Symbol('SHIELD_SINGLE_LAYOUT_PROBE');
|
|
393
|
+
/**
|
|
394
|
+
* Single-database-layout spec §5.F (ADR-0041): the privilege probe runs once at bootstrap on this
|
|
395
|
+
* module's runtime pool, single layout only (four layout: a no-op hook). Never blocks boot.
|
|
396
|
+
*/
|
|
397
|
+
exports.singleLayoutProbeProvider = {
|
|
398
|
+
provide: exports.SHIELD_SINGLE_LAYOUT_PROBE,
|
|
399
|
+
useFactory: (pool, config) => {
|
|
400
|
+
const schemas = (0, app_config_js_1.frameworkSchemasOf)(config);
|
|
401
|
+
if (schemas.layout !== 'single')
|
|
402
|
+
return { onApplicationBootstrap: () => undefined };
|
|
403
|
+
return new single_layout_probe_js_1.SingleLayoutProbeBoot(async (sql, params) => (await pool.query(sql, params)), schemas.app);
|
|
404
|
+
},
|
|
405
|
+
inject: [tokens_js_1.SHIELD_DB_POOL, config_module_js_1.APP_CONFIG],
|
|
406
|
+
};
|
|
390
407
|
/** Boot-time warnings from the auth composition (dev email logger, disabled email, …). */
|
|
391
408
|
const authBootLogger = new common_1.Logger('ShieldAuth');
|
|
392
409
|
/**
|
|
@@ -1029,6 +1046,7 @@ let AuthModule = AuthModule_1 = class AuthModule {
|
|
|
1029
1046
|
resetLimiterProvider,
|
|
1030
1047
|
// spec 5.D: the guard migrations are in the ledger (once at bootstrap; production fails).
|
|
1031
1048
|
migrationLedgerCheckProvider,
|
|
1049
|
+
exports.singleLayoutProbeProvider,
|
|
1032
1050
|
],
|
|
1033
1051
|
exports: [
|
|
1034
1052
|
tokens_js_1.SHIELD_AUTHENTICATOR,
|
|
@@ -35,6 +35,12 @@ export declare class BootSequence implements OnApplicationBootstrap {
|
|
|
35
35
|
}
|
|
36
36
|
/** The boot log line naming the resolved framework databases (names only, spec 5.E). */
|
|
37
37
|
export declare function frameworkDatabasesLogLine(s: FrameworkSchemas): string;
|
|
38
|
+
/**
|
|
39
|
+
* Single-database-layout spec §5.C, AC-1 (ADR-0041): the warning for `SHIELD_DB_NAME` set under
|
|
40
|
+
* the four layout (it is read only when `SHIELD_DB_LAYOUT=single`), else `undefined`. Names the
|
|
41
|
+
* variable only, never its value.
|
|
42
|
+
*/
|
|
43
|
+
export declare function shieldDbNameIgnoredWarning(env: NodeJS.ProcessEnv): string | undefined;
|
|
38
44
|
/**
|
|
39
45
|
* Validate a {@link PoolBudgetConfig} against the ADR-0018 budget. Exported so
|
|
40
46
|
* unit tests can assert the throw/no-throw contract directly without booting
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"boot-sequence.d.ts","sourceRoot":"","sources":["../../src/boot/boot-sequence.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACzF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;;;;GAcG;AACH,qBACa,YAAa,YAAW,sBAAsB;IAGzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;gBAEE,MAAM,EAAE,SAAS;IAE5D,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"boot-sequence.d.ts","sourceRoot":"","sources":["../../src/boot/boot-sequence.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACzF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;;;;GAcG;AACH,qBACa,YAAa,YAAW,sBAAsB;IAGzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;gBAEE,MAAM,EAAE,SAAS;IAE5D,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAa7C;;;;;;;;;OASG;YACW,yBAAyB;CAIxC;AAED,wFAAwF;AACxF,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAMrE;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,GAAG,SAAS,CAKrF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAsC5D"}
|
|
@@ -14,6 +14,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.BootSequence = void 0;
|
|
16
16
|
exports.frameworkDatabasesLogLine = frameworkDatabasesLogLine;
|
|
17
|
+
exports.shieldDbNameIgnoredWarning = shieldDbNameIgnoredWarning;
|
|
17
18
|
exports.assertPoolBudget = assertPoolBudget;
|
|
18
19
|
const common_1 = require("@nestjs/common");
|
|
19
20
|
const config_module_js_1 = require("../config/config.module.js");
|
|
@@ -43,6 +44,10 @@ let BootSequence = class BootSequence {
|
|
|
43
44
|
// configurable-framework-db spec 5.E: the resolved database names, once per boot. Names
|
|
44
45
|
// only — never a user, password, host or connection string.
|
|
45
46
|
this.logger.log(frameworkDatabasesLogLine((0, app_config_js_1.frameworkSchemasOf)(this.config)));
|
|
47
|
+
// Spec 5.C/AC-1: SHIELD_DB_NAME is read only in the single layout; say so when it is ignored.
|
|
48
|
+
const ignored = shieldDbNameIgnoredWarning(process.env);
|
|
49
|
+
if (ignored !== undefined)
|
|
50
|
+
this.logger.warn(ignored);
|
|
46
51
|
await this.assertConnectionPoolReady();
|
|
47
52
|
// Future boot-time assertions attach here (each fail-closed). The migration ledger check
|
|
48
53
|
// (spec 5.D) needs the runtime pool, so it runs from AuthModule (`auth: true`) and from
|
|
@@ -71,9 +76,24 @@ exports.BootSequence = BootSequence = __decorate([
|
|
|
71
76
|
], BootSequence);
|
|
72
77
|
/** The boot log line naming the resolved framework databases (names only, spec 5.E). */
|
|
73
78
|
function frameworkDatabasesLogLine(s) {
|
|
79
|
+
if (s.layout === 'single')
|
|
80
|
+
return `Shield databases: layout=single database=${s.app}`;
|
|
74
81
|
return (`Shield databases: namespace=${s.namespace} app=${s.app} identity=${s.identity} ` +
|
|
75
82
|
`authz=${s.authz} audit=${s.audit}`);
|
|
76
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Single-database-layout spec §5.C, AC-1 (ADR-0041): the warning for `SHIELD_DB_NAME` set under
|
|
86
|
+
* the four layout (it is read only when `SHIELD_DB_LAYOUT=single`), else `undefined`. Names the
|
|
87
|
+
* variable only, never its value.
|
|
88
|
+
*/
|
|
89
|
+
function shieldDbNameIgnoredWarning(env) {
|
|
90
|
+
if (env.SHIELD_DB_LAYOUT === 'single')
|
|
91
|
+
return undefined;
|
|
92
|
+
const v = env.SHIELD_DB_NAME;
|
|
93
|
+
if (v === undefined || v.trim() === '')
|
|
94
|
+
return undefined;
|
|
95
|
+
return 'SHIELD_DB_NAME is read only when SHIELD_DB_LAYOUT=single; ignored.';
|
|
96
|
+
}
|
|
77
97
|
/**
|
|
78
98
|
* Validate a {@link PoolBudgetConfig} against the ADR-0018 budget. Exported so
|
|
79
99
|
* unit tests can assert the throw/no-throw contract directly without booting
|
|
@@ -90,13 +110,25 @@ function assertPoolBudget(cfg) {
|
|
|
90
110
|
'which exceeds the hard cap of 2. DDL is rare + serialized per tenant ' +
|
|
91
111
|
'(ADR-0014 §7) — keep this pool tiny (1–2).');
|
|
92
112
|
}
|
|
113
|
+
if (!e.withinUserBudget) {
|
|
114
|
+
throw new Error('[ADR-0018] connection-pool assertion FAILED (fail-closed): single layout needs ' +
|
|
115
|
+
`${e.requiredConnections} connections (workers(${cfg.workers}) × ` +
|
|
116
|
+
`${e.perWorkerConnections} + headroom(${cfg.headroom})) but the account allows ` +
|
|
117
|
+
`${cfg.maxUserConnections} (APP_DB_MAX_USER_CONNECTIONS). Reduce POOL_MAX_CORE, ` +
|
|
118
|
+
'POOL_MAX_ENGINE, APP_AUTHZ_POOL_MAX, POOL_WORKERS or POOL_HEADROOM, or raise ' +
|
|
119
|
+
'MAX_USER_CONNECTIONS on the account (ADR-0041).');
|
|
120
|
+
}
|
|
93
121
|
if (!e.withinBudget) {
|
|
94
122
|
throw new Error('[ADR-0018] connection-pool assertion FAILED (fail-closed): required ' +
|
|
95
123
|
`connections ${e.requiredConnections} exceed max_connections ` +
|
|
96
124
|
`${cfg.maxConnections}. Budget = workers(${cfg.workers}) × ` +
|
|
97
125
|
`Σ(per-account pool max)(${e.perWorkerConnections}) + headroom(` +
|
|
98
126
|
`${cfg.headroom}) = ${e.requiredConnections} > ${cfg.maxConnections}. ` +
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
(cfg.layout === 'single'
|
|
128
|
+
? 'Single layout: one account opens POOL_MAX_CORE + POOL_MAX_ENGINE (unset ⇒ ' +
|
|
129
|
+
'APP_DB_MAX_CONNECTIONS, else 10) + APP_AUTHZ_POOL_MAX per worker; set POOL_MAX_ENGINE ' +
|
|
130
|
+
'explicitly, lower a pool cap or the worker count, or raise max_connections.'
|
|
131
|
+
: 'Reduce a per-account pool cap, the worker count, or raise ' +
|
|
132
|
+
'max_connections (and MariaDB my.cnf) to restore the budget.'));
|
|
101
133
|
}
|
|
102
134
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-database-layout boot privilege probe (single-database-layout spec §5.F, Req 7, AC-8,
|
|
3
|
+
* ADR-0041).
|
|
4
|
+
*
|
|
5
|
+
* With `SHIELD_DB_LAYOUT=single` the four framework modules share one database, so the isolation
|
|
6
|
+
* the four-database grant matrix gave is partly gone. Which protections are really lost depends on
|
|
7
|
+
* what the runtime account may do, so this reads (never writes) `information_schema`
|
|
8
|
+
* USER_PRIVILEGES / SCHEMA_PRIVILEGES / TABLE_PRIVILEGES / COLUMN_PRIVILEGES / APPLICABLE_ROLES for `CURRENT_USER()`, once at boot, and
|
|
9
|
+
* derives the five protections lost (spec §5.F). It does not look for `prune_audit`: MariaDB hides
|
|
10
|
+
* a routine from an account without EXECUTE on it, which the runtime account must not have (spec
|
|
11
|
+
* v3); `audit:prune` reports a missing routine itself.
|
|
12
|
+
*
|
|
13
|
+
* `GRANT ... TO PUBLIC` (MariaDB 10.11+) applies to every account but information_schema shows
|
|
14
|
+
* those rows only to privileged accounts (verified on 10.11 and 11.4: a plain account sees none).
|
|
15
|
+
* `SHOW GRANTS` for the current user needs no privilege and does list them, so its `TO PUBLIC`
|
|
16
|
+
* lines are parsed and merged with the information_schema rows. Its routine-level lines
|
|
17
|
+
* (`ON PROCEDURE|FUNCTION`) are the only view of routine privileges the account has: any such grant
|
|
18
|
+
* on the database loses the retention floor. An unparsable line of the account's own is ignored, an
|
|
19
|
+
* unparsable PUBLIC or routine line fails closed; a failing `SHOW GRANTS`
|
|
20
|
+
* fails closed like any other failed read.
|
|
21
|
+
*
|
|
22
|
+
* The probe never blocks boot (the operator's ack already accepted the risk) and is never on the
|
|
23
|
+
* request path (Shield #12). Warning lines carry no credential and no host (Shield #10): a failed
|
|
24
|
+
* query's driver message is dropped.
|
|
25
|
+
*/
|
|
26
|
+
import { Logger, type OnApplicationBootstrap } from '@nestjs/common';
|
|
27
|
+
/** Runs one parameterized read on the runtime pool (a mariadb `Pool.query` fits). */
|
|
28
|
+
export type ProbeQuery = (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
29
|
+
/** Id of a protection the single layout can lose. */
|
|
30
|
+
export type LostProtectionId = 'append-only-audit' | 'no-runtime-ddl' | 'schema-separation' | 'retention-floor' | 'separate-ddl-account';
|
|
31
|
+
/** One lost protection: id, the invariant it belongs to, and the operator-facing text. */
|
|
32
|
+
export interface LostProtection {
|
|
33
|
+
readonly id: LostProtectionId;
|
|
34
|
+
/** The Shield invariant citation, e.g. `Shield #6`. */
|
|
35
|
+
readonly invariant: string;
|
|
36
|
+
readonly text: string;
|
|
37
|
+
}
|
|
38
|
+
/** The probe outcome. `readFailed` ⇒ the privileges could not be read (all five reported). */
|
|
39
|
+
export interface SingleLayoutProbe {
|
|
40
|
+
readonly lost: readonly LostProtection[];
|
|
41
|
+
readonly readFailed: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Derive the protections the runtime account has lost. Never throws, and never reports the audit
|
|
45
|
+
* wall intact when it cannot see the grants: a failed query, a runtime account whose own
|
|
46
|
+
* SELECT+INSERT on the database is invisible, or an account with roles (not inspected) reports all
|
|
47
|
+
* five with `readFailed`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function probeSingleLayoutPrivileges(query: ProbeQuery, database: string): Promise<SingleLayoutProbe>;
|
|
50
|
+
/**
|
|
51
|
+
* The boot warning: a header naming the database and ADR-0041, then one line per lost protection
|
|
52
|
+
* citing its invariant. No credential, host or driver message.
|
|
53
|
+
*/
|
|
54
|
+
export declare function singleLayoutWarningLines(probe: SingleLayoutProbe, database: string): string[];
|
|
55
|
+
/**
|
|
56
|
+
* Runs the probe once at application bootstrap on the runtime pool and logs the warning. Never
|
|
57
|
+
* throws, so boot is never blocked (spec §5.F).
|
|
58
|
+
*/
|
|
59
|
+
export declare class SingleLayoutProbeBoot implements OnApplicationBootstrap {
|
|
60
|
+
private readonly query;
|
|
61
|
+
private readonly database;
|
|
62
|
+
private readonly logger;
|
|
63
|
+
constructor(query: ProbeQuery, database: string, logger?: Pick<Logger, 'warn'>);
|
|
64
|
+
onApplicationBootstrap(): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=single-layout-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"single-layout-probe.d.ts","sourceRoot":"","sources":["../../src/boot/single-layout-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,MAAM,EAAE,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAErE,qFAAqF;AACrF,MAAM,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;AAEjF,qDAAqD;AACrD,MAAM,MAAM,gBAAgB,GACxB,mBAAmB,GACnB,gBAAgB,GAChB,mBAAmB,GACnB,iBAAiB,GACjB,sBAAsB,CAAC;AAE3B,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,gBAAgB,CAAC;IAC9B,uDAAuD;IACvD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,8FAA8F;AAC9F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,CAAC;IACzC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAoMD;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,iBAAiB,CAAC,CAoG5B;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAsB7F;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,sBAAsB;IAEhE,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAFN,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAwB;IAGhE,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;CAI9C"}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SingleLayoutProbeBoot = void 0;
|
|
4
|
+
exports.probeSingleLayoutPrivileges = probeSingleLayoutPrivileges;
|
|
5
|
+
exports.singleLayoutWarningLines = singleLayoutWarningLines;
|
|
6
|
+
/**
|
|
7
|
+
* Single-database-layout boot privilege probe (single-database-layout spec §5.F, Req 7, AC-8,
|
|
8
|
+
* ADR-0041).
|
|
9
|
+
*
|
|
10
|
+
* With `SHIELD_DB_LAYOUT=single` the four framework modules share one database, so the isolation
|
|
11
|
+
* the four-database grant matrix gave is partly gone. Which protections are really lost depends on
|
|
12
|
+
* what the runtime account may do, so this reads (never writes) `information_schema`
|
|
13
|
+
* USER_PRIVILEGES / SCHEMA_PRIVILEGES / TABLE_PRIVILEGES / COLUMN_PRIVILEGES / APPLICABLE_ROLES for `CURRENT_USER()`, once at boot, and
|
|
14
|
+
* derives the five protections lost (spec §5.F). It does not look for `prune_audit`: MariaDB hides
|
|
15
|
+
* a routine from an account without EXECUTE on it, which the runtime account must not have (spec
|
|
16
|
+
* v3); `audit:prune` reports a missing routine itself.
|
|
17
|
+
*
|
|
18
|
+
* `GRANT ... TO PUBLIC` (MariaDB 10.11+) applies to every account but information_schema shows
|
|
19
|
+
* those rows only to privileged accounts (verified on 10.11 and 11.4: a plain account sees none).
|
|
20
|
+
* `SHOW GRANTS` for the current user needs no privilege and does list them, so its `TO PUBLIC`
|
|
21
|
+
* lines are parsed and merged with the information_schema rows. Its routine-level lines
|
|
22
|
+
* (`ON PROCEDURE|FUNCTION`) are the only view of routine privileges the account has: any such grant
|
|
23
|
+
* on the database loses the retention floor. An unparsable line of the account's own is ignored, an
|
|
24
|
+
* unparsable PUBLIC or routine line fails closed; a failing `SHOW GRANTS`
|
|
25
|
+
* fails closed like any other failed read.
|
|
26
|
+
*
|
|
27
|
+
* The probe never blocks boot (the operator's ack already accepted the risk) and is never on the
|
|
28
|
+
* request path (Shield #12). Warning lines carry no credential and no host (Shield #10): a failed
|
|
29
|
+
* query's driver message is dropped.
|
|
30
|
+
*/
|
|
31
|
+
const common_1 = require("@nestjs/common");
|
|
32
|
+
const PROTECTIONS = {
|
|
33
|
+
'append-only-audit': {
|
|
34
|
+
id: 'append-only-audit',
|
|
35
|
+
invariant: 'Shield #6',
|
|
36
|
+
text: 'Append-only audit: the runtime account can UPDATE or DELETE audit_log.',
|
|
37
|
+
},
|
|
38
|
+
'no-runtime-ddl': {
|
|
39
|
+
id: 'no-runtime-ddl',
|
|
40
|
+
invariant: 'Shield #11',
|
|
41
|
+
text: 'No runtime DDL: the runtime account holds DDL privileges on the database.',
|
|
42
|
+
},
|
|
43
|
+
'schema-separation': {
|
|
44
|
+
id: 'schema-separation',
|
|
45
|
+
invariant: 'Shield #11 (ADR-0014)',
|
|
46
|
+
text: 'Schema separation: identity, authz and audit tables share one database with app tables ' +
|
|
47
|
+
'(always lost in the single layout).',
|
|
48
|
+
},
|
|
49
|
+
'retention-floor': {
|
|
50
|
+
id: 'retention-floor',
|
|
51
|
+
invariant: 'Shield #6',
|
|
52
|
+
text: 'DB-enforced retention floor: audit rows can be deleted directly, around prune_audit, or ' +
|
|
53
|
+
'the runtime account holds a grant on prune_audit.',
|
|
54
|
+
},
|
|
55
|
+
'separate-ddl-account': {
|
|
56
|
+
id: 'separate-ddl-account',
|
|
57
|
+
invariant: 'Shield #11',
|
|
58
|
+
text: 'Separate deploy-time DDL account: the runtime account can run migrations, so locking the ' +
|
|
59
|
+
'migrate account between deploys is not possible.',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const ALL_IDS = Object.keys(PROTECTIONS);
|
|
63
|
+
/** Aligned with `scripts/db-grants/lib.mjs` DDL_PRIVS (GRANT OPTION is judged via IS_GRANTABLE too). */
|
|
64
|
+
const DDL_PRIVILEGES = new Set([
|
|
65
|
+
'CREATE',
|
|
66
|
+
'ALTER',
|
|
67
|
+
'DROP',
|
|
68
|
+
'INDEX',
|
|
69
|
+
'CREATE VIEW',
|
|
70
|
+
'SHOW VIEW',
|
|
71
|
+
'TRIGGER',
|
|
72
|
+
'CREATE ROUTINE',
|
|
73
|
+
'ALTER ROUTINE',
|
|
74
|
+
'EVENT',
|
|
75
|
+
'REFERENCES',
|
|
76
|
+
'CREATE TEMPORARY TABLES',
|
|
77
|
+
'LOCK TABLES',
|
|
78
|
+
'DELETE HISTORY',
|
|
79
|
+
'GRANT OPTION',
|
|
80
|
+
]);
|
|
81
|
+
const AUDIT_TABLE = 'audit_log';
|
|
82
|
+
/** `'user'@'host'` for CURRENT_USER(), built in SQL (never from env). */
|
|
83
|
+
const GRANTEE_SQL = `CONCAT("'", SUBSTRING_INDEX(CURRENT_USER(),'@',1), "'@'", ` +
|
|
84
|
+
`SUBSTRING_INDEX(CURRENT_USER(),'@',-1), "'")`;
|
|
85
|
+
/** Does a GRANT schema pattern (`\_` escaped, `_`/`%` wildcards) cover `database`? */
|
|
86
|
+
function schemaCovers(pattern, database) {
|
|
87
|
+
let re = '';
|
|
88
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
89
|
+
const c = pattern[i];
|
|
90
|
+
if (c === '\\' && i + 1 < pattern.length) {
|
|
91
|
+
re += pattern[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
92
|
+
}
|
|
93
|
+
else if (c === '%')
|
|
94
|
+
re += '.*';
|
|
95
|
+
else if (c === '_')
|
|
96
|
+
re += '.';
|
|
97
|
+
else
|
|
98
|
+
re += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
}
|
|
100
|
+
return new RegExp(`^${re}$`, 'i').test(database);
|
|
101
|
+
}
|
|
102
|
+
/** Splits `a, b (c, d), e` at top-level commas, respecting backticks and parentheses. */
|
|
103
|
+
function splitTopLevel(text) {
|
|
104
|
+
const out = [];
|
|
105
|
+
let cur = '';
|
|
106
|
+
let depth = 0;
|
|
107
|
+
let tick = false;
|
|
108
|
+
for (const c of text) {
|
|
109
|
+
if (c === '`')
|
|
110
|
+
tick = !tick;
|
|
111
|
+
else if (!tick && c === '(')
|
|
112
|
+
depth++;
|
|
113
|
+
else if (!tick && c === ')')
|
|
114
|
+
depth = Math.max(0, depth - 1);
|
|
115
|
+
if (c === ',' && depth === 0 && !tick) {
|
|
116
|
+
out.push(cur);
|
|
117
|
+
cur = '';
|
|
118
|
+
}
|
|
119
|
+
else
|
|
120
|
+
cur += c;
|
|
121
|
+
}
|
|
122
|
+
out.push(cur);
|
|
123
|
+
return out.map((x) => x.trim()).filter((x) => x.length > 0);
|
|
124
|
+
}
|
|
125
|
+
// `*`, backtick-quoted, ANSI_QUOTES double-quoted, or bare (sql_quote_show_create=0).
|
|
126
|
+
const IDENT = '(\\*|`(?:[^`]|``)*`|"(?:[^"]|"")*"|[^\\s.`"]+)';
|
|
127
|
+
const GRANT_LINE = /^GRANT\s+(.+?)\s+ON\s+(.+?)\s+TO\s+(.+?)(\s+WITH\s+GRANT\s+OPTION)?\s*$/is;
|
|
128
|
+
const TARGET = new RegExp(`^(?:(PROCEDURE|FUNCTION|PACKAGE|PACKAGE BODY)\\s+)?${IDENT}\\.${IDENT}$`, 'i');
|
|
129
|
+
const unquote = (id) => id.startsWith('`')
|
|
130
|
+
? id.slice(1, -1).replace(/``/g, '`')
|
|
131
|
+
: id.startsWith('"')
|
|
132
|
+
? id.slice(1, -1).replace(/""/g, '"')
|
|
133
|
+
: id;
|
|
134
|
+
const ROUTINE_TARGET = /^(PROCEDURE|FUNCTION|PACKAGE)\b/i;
|
|
135
|
+
/**
|
|
136
|
+
* Reads `TO PUBLIC` privileges and routine-level grants out of `SHOW GRANTS` lines. An
|
|
137
|
+
* unparsable `TO PUBLIC` or routine line sets `unreadable` (the probe then fails closed); any
|
|
138
|
+
* other unparsable line is the account's own and is skipped. Never throws.
|
|
139
|
+
*/
|
|
140
|
+
function parseShowGrants(lines) {
|
|
141
|
+
const out = {
|
|
142
|
+
user: [],
|
|
143
|
+
schema: [],
|
|
144
|
+
table: [],
|
|
145
|
+
column: [],
|
|
146
|
+
routineSchemas: [],
|
|
147
|
+
unreadable: false,
|
|
148
|
+
};
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const m = GRANT_LINE.exec(line);
|
|
151
|
+
if (!m)
|
|
152
|
+
continue;
|
|
153
|
+
const rawTarget = m[2].trim();
|
|
154
|
+
const target = TARGET.exec(rawTarget);
|
|
155
|
+
if (!target) {
|
|
156
|
+
if (/^PUBLIC$/i.test(m[3].trim()) || ROUTINE_TARGET.test(rawTarget)) {
|
|
157
|
+
out.unreadable = true;
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const kind = target[1];
|
|
162
|
+
const schema = unquote(target[2]);
|
|
163
|
+
const name = unquote(target[3]);
|
|
164
|
+
if (kind) {
|
|
165
|
+
out.routineSchemas.push(schema);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!/^PUBLIC$/i.test(m[3].trim()))
|
|
169
|
+
continue;
|
|
170
|
+
const is = m[4] ? 'YES' : 'NO';
|
|
171
|
+
for (const item of splitTopLevel(m[1])) {
|
|
172
|
+
const paren = item.indexOf('(');
|
|
173
|
+
const type = (paren < 0 ? item : item.slice(0, paren)).trim().toUpperCase();
|
|
174
|
+
if (!type)
|
|
175
|
+
continue;
|
|
176
|
+
if (paren >= 0) {
|
|
177
|
+
// Column-level: one row per column (the name only matters for the table).
|
|
178
|
+
out.column.push({
|
|
179
|
+
TABLE_SCHEMA: schema,
|
|
180
|
+
TABLE_NAME: name,
|
|
181
|
+
PRIVILEGE_TYPE: type,
|
|
182
|
+
IS_GRANTABLE: is,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else if (schema === '*') {
|
|
186
|
+
out.user.push({ PRIVILEGE_TYPE: type, IS_GRANTABLE: is });
|
|
187
|
+
}
|
|
188
|
+
else if (name === '*') {
|
|
189
|
+
out.schema.push({ TABLE_SCHEMA: schema, PRIVILEGE_TYPE: type, IS_GRANTABLE: is });
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
out.table.push({
|
|
193
|
+
TABLE_SCHEMA: schema,
|
|
194
|
+
TABLE_NAME: name,
|
|
195
|
+
PRIVILEGE_TYPE: type,
|
|
196
|
+
IS_GRANTABLE: is,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
const priv = (r) => String(r.PRIVILEGE_TYPE ?? '').toUpperCase();
|
|
204
|
+
const grantable = (r) => String(r.IS_GRANTABLE ?? '').toUpperCase() === 'YES';
|
|
205
|
+
const failed = () => ({
|
|
206
|
+
lost: ALL_IDS.map((id) => PROTECTIONS[id]),
|
|
207
|
+
readFailed: true,
|
|
208
|
+
});
|
|
209
|
+
/**
|
|
210
|
+
* Derive the protections the runtime account has lost. Never throws, and never reports the audit
|
|
211
|
+
* wall intact when it cannot see the grants: a failed query, a runtime account whose own
|
|
212
|
+
* SELECT+INSERT on the database is invisible, or an account with roles (not inspected) reports all
|
|
213
|
+
* five with `readFailed`.
|
|
214
|
+
*/
|
|
215
|
+
async function probeSingleLayoutPrivileges(query, database) {
|
|
216
|
+
try {
|
|
217
|
+
const userInfo = (await query(`SELECT PRIVILEGE_TYPE, IS_GRANTABLE FROM information_schema.USER_PRIVILEGES ` +
|
|
218
|
+
`WHERE GRANTEE = ${GRANTEE_SQL}`));
|
|
219
|
+
const schemaInfo = (await query(`SELECT TABLE_SCHEMA, PRIVILEGE_TYPE, IS_GRANTABLE FROM information_schema.SCHEMA_PRIVILEGES ` +
|
|
220
|
+
`WHERE GRANTEE = ${GRANTEE_SQL}`));
|
|
221
|
+
const tableInfo = (await query(`SELECT TABLE_SCHEMA, TABLE_NAME, PRIVILEGE_TYPE, IS_GRANTABLE ` +
|
|
222
|
+
`FROM information_schema.TABLE_PRIVILEGES WHERE GRANTEE = ${GRANTEE_SQL}`));
|
|
223
|
+
const columnInfo = (await query(`SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, PRIVILEGE_TYPE, IS_GRANTABLE ` +
|
|
224
|
+
`FROM information_schema.COLUMN_PRIVILEGES WHERE GRANTEE = ${GRANTEE_SQL}`));
|
|
225
|
+
const roles = (await query(`SELECT ROLE_NAME FROM information_schema.APPLICABLE_ROLES WHERE GRANTEE = ${GRANTEE_SQL}`));
|
|
226
|
+
// Privileges granted through roles are not inspected: do not claim a wall we cannot see.
|
|
227
|
+
if (roles.length > 0)
|
|
228
|
+
return failed();
|
|
229
|
+
// PUBLIC grants and routine grants: invisible in information_schema to this account.
|
|
230
|
+
const shown = (await query('SHOW GRANTS'));
|
|
231
|
+
const pub = parseShowGrants(shown.map((row) => String(Object.values(row ?? {})[0] ?? '')).filter((l) => l.length > 0));
|
|
232
|
+
if (pub.unreadable)
|
|
233
|
+
return failed();
|
|
234
|
+
const user = [...userInfo, ...pub.user];
|
|
235
|
+
const schema = [...schemaInfo, ...pub.schema];
|
|
236
|
+
const table = [...tableInfo, ...pub.table];
|
|
237
|
+
const column = [...columnInfo, ...pub.column];
|
|
238
|
+
const inDb = (r) => schemaCovers(String(r.TABLE_SCHEMA ?? ''), database);
|
|
239
|
+
const schemaRows = schema.filter(inDb);
|
|
240
|
+
const tables = table.filter(inDb);
|
|
241
|
+
const columns = column.filter(inDb);
|
|
242
|
+
// Any routine-level grant on the database (EXECUTE, ALTER ROUTINE, ...): prune_audit is reachable.
|
|
243
|
+
const routineGrant = pub.routineSchemas.some((sch) => schemaCovers(sch, database));
|
|
244
|
+
// Privileges that reach the whole database: global plus matching schema grants.
|
|
245
|
+
const db = new Set([...user.map(priv), ...schemaRows.map(priv)]);
|
|
246
|
+
const all = db.has('ALL PRIVILEGES') || db.has('ALL');
|
|
247
|
+
// The account booted, so it holds SELECT and INSERT on the database. Not seeing them means
|
|
248
|
+
// the grants are invisible (restricted view, odd grantee form): fail closed.
|
|
249
|
+
if (!all && !(db.has('SELECT') && db.has('INSERT')))
|
|
250
|
+
return failed();
|
|
251
|
+
// WITH GRANT OPTION anywhere on the database: the account can give itself anything.
|
|
252
|
+
const selfGrant = [...user, ...schemaRows, ...tables, ...columns].some(grantable);
|
|
253
|
+
const auditWrite = selfGrant ||
|
|
254
|
+
all ||
|
|
255
|
+
db.has('UPDATE') ||
|
|
256
|
+
db.has('DELETE') ||
|
|
257
|
+
// DROP allows TRUNCATE and dropping audit_log; ALTER rewrites it.
|
|
258
|
+
db.has('DROP') ||
|
|
259
|
+
db.has('ALTER') ||
|
|
260
|
+
tables.some((r) => String(r.TABLE_NAME ?? '').toLowerCase() === AUDIT_TABLE &&
|
|
261
|
+
['UPDATE', 'DELETE', 'DROP', 'ALTER', 'ALL PRIVILEGES'].includes(priv(r))) ||
|
|
262
|
+
// Column-level UPDATE rewrites audit rows (there is no column-level DELETE).
|
|
263
|
+
columns.some((r) => String(r.TABLE_NAME ?? '').toLowerCase() === AUDIT_TABLE && priv(r) === 'UPDATE');
|
|
264
|
+
const ddl = selfGrant ||
|
|
265
|
+
all ||
|
|
266
|
+
[...db].some((p) => DDL_PRIVILEGES.has(p)) ||
|
|
267
|
+
tables.some((r) => DDL_PRIVILEGES.has(priv(r)));
|
|
268
|
+
// Database-level or global EXECUTE reaches prune_audit.
|
|
269
|
+
const execute = db.has('EXECUTE');
|
|
270
|
+
const lost = new Set(['schema-separation']);
|
|
271
|
+
if (auditWrite || routineGrant || execute) {
|
|
272
|
+
// The floor lives in prune_audit; a direct write on audit_log, or any grant on a routine
|
|
273
|
+
// of the database (EXECUTE / ALTER ROUTINE on prune_audit), gets around or weakens it.
|
|
274
|
+
lost.add('retention-floor');
|
|
275
|
+
}
|
|
276
|
+
if (auditWrite)
|
|
277
|
+
lost.add('append-only-audit');
|
|
278
|
+
if (ddl) {
|
|
279
|
+
lost.add('no-runtime-ddl');
|
|
280
|
+
lost.add('separate-ddl-account');
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
lost: ALL_IDS.filter((id) => lost.has(id)).map((id) => PROTECTIONS[id]),
|
|
284
|
+
readFailed: false,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return failed();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The boot warning: a header naming the database and ADR-0041, then one line per lost protection
|
|
293
|
+
* citing its invariant. No credential, host or driver message.
|
|
294
|
+
*/
|
|
295
|
+
function singleLayoutWarningLines(probe, database) {
|
|
296
|
+
const lines = [
|
|
297
|
+
`Shield single database layout on "${database}" (ADR-0041): reduced isolation accepted; ` +
|
|
298
|
+
`protections lost:`,
|
|
299
|
+
];
|
|
300
|
+
if (probe.readFailed) {
|
|
301
|
+
lines.push('Runtime privileges could not be read; assuming all protections are lost.');
|
|
302
|
+
}
|
|
303
|
+
for (const p of probe.lost)
|
|
304
|
+
lines.push(`- ${p.text} (${p.invariant})`);
|
|
305
|
+
const ids = new Set(probe.lost.map((p) => p.id));
|
|
306
|
+
if (!probe.readFailed &&
|
|
307
|
+
!ids.has('append-only-audit') &&
|
|
308
|
+
!ids.has('no-runtime-ddl') &&
|
|
309
|
+
!ids.has('retention-floor')) {
|
|
310
|
+
lines.push('Two-account setup: audit wall intact (no direct UPDATE/DELETE/DDL/grant rights seen for ' +
|
|
311
|
+
'this account).');
|
|
312
|
+
}
|
|
313
|
+
return lines;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Runs the probe once at application bootstrap on the runtime pool and logs the warning. Never
|
|
317
|
+
* throws, so boot is never blocked (spec §5.F).
|
|
318
|
+
*/
|
|
319
|
+
class SingleLayoutProbeBoot {
|
|
320
|
+
query;
|
|
321
|
+
database;
|
|
322
|
+
logger;
|
|
323
|
+
constructor(query, database, logger = new common_1.Logger('Shield')) {
|
|
324
|
+
this.query = query;
|
|
325
|
+
this.database = database;
|
|
326
|
+
this.logger = logger;
|
|
327
|
+
}
|
|
328
|
+
async onApplicationBootstrap() {
|
|
329
|
+
const probe = await probeSingleLayoutPrivileges(this.query, this.database);
|
|
330
|
+
for (const line of singleLayoutWarningLines(probe, this.database))
|
|
331
|
+
this.logger.warn(line);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
exports.SingleLayoutProbeBoot = SingleLayoutProbeBoot;
|
|
@@ -182,6 +182,18 @@ export interface PoolBudgetConfig {
|
|
|
182
182
|
readonly headroom: number;
|
|
183
183
|
/** Per-account RUNTIME pool caps (multiplied by `workers` in the budget). */
|
|
184
184
|
readonly perAccountMax: RuntimeAccountPools;
|
|
185
|
+
/**
|
|
186
|
+
* `'single'` when `SHIELD_DB_LAYOUT=single` (single-database-layout spec §5.C, AC-12, ADR-0041):
|
|
187
|
+
* one account opens the core, engine and authz pools, so the budget sums those instead.
|
|
188
|
+
* Absent in the four layout (today's math, unchanged).
|
|
189
|
+
*/
|
|
190
|
+
readonly layout?: 'single';
|
|
191
|
+
/** Single layout: the engine pool cap (`POOL_MAX_ENGINE ?? APP_DB_MAX_CONNECTIONS ?? 10`). */
|
|
192
|
+
readonly engineMax?: number;
|
|
193
|
+
/** Single layout: the identity authz pool cap (`APP_AUTHZ_POOL_MAX ?? 4`). */
|
|
194
|
+
readonly authzMax?: number;
|
|
195
|
+
/** Single layout: the account's `MAX_USER_CONNECTIONS` (`APP_DB_MAX_USER_CONNECTIONS`), if set. */
|
|
196
|
+
readonly maxUserConnections?: number;
|
|
185
197
|
}
|
|
186
198
|
/**
|
|
187
199
|
* Per-account maximum pool sizes for the RUNTIME accounts (the ones each worker
|
|
@@ -393,6 +405,11 @@ export interface PoolBudgetEvaluation {
|
|
|
393
405
|
readonly withinBudget: boolean;
|
|
394
406
|
/** True iff shield_schema_svc pool ≤ {@link SCHEMA_SVC_POOL_HARD_CAP}. */
|
|
395
407
|
readonly schemaSvcWithinHardCap: boolean;
|
|
408
|
+
/**
|
|
409
|
+
* True iff requiredConnections ≤ `maxUserConnections` (always true when unset or in the four
|
|
410
|
+
* layout). Single layout only (spec AC-12).
|
|
411
|
+
*/
|
|
412
|
+
readonly withinUserBudget: boolean;
|
|
396
413
|
}
|
|
397
414
|
/**
|
|
398
415
|
* Evaluate a pool budget. Pure: no throwing, no I/O — the caller decides how to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAWtE,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,aAAa,GAAG,MAAM,GAAG,YAAY,CAAC;IAC5D,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAGhC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,wBAAwB,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,gBAAgB,CAEvF;AAED,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,EAAE,kBAGpC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,WAAW,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,8FAA8F;AAC9F,eAAO,MAAM,gCAAgC,OAAO,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CACxC;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -84,6 +84,21 @@ function parsePositiveInt(raw, fallback) {
|
|
|
84
84
|
const n = Number.parseInt(raw, 10);
|
|
85
85
|
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A pool-size variable: unset or blank gives `undefined`; otherwise a positive integer in plain
|
|
89
|
+
* digits (surrounding whitespace allowed) or it throws naming the variable (never the value; fail
|
|
90
|
+
* closed). Identity's engine and authz pools apply the same rule, so the budget counts exactly the
|
|
91
|
+
* pools that open.
|
|
92
|
+
*/
|
|
93
|
+
function strictOptionalPoolInt(raw, name) {
|
|
94
|
+
if (raw === undefined || raw.trim() === '')
|
|
95
|
+
return undefined;
|
|
96
|
+
const t = raw.trim();
|
|
97
|
+
const n = /^[0-9]+$/.test(t) ? Number(t) : Number.NaN;
|
|
98
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
99
|
+
throw new Error(`${name} must be a positive integer`);
|
|
100
|
+
return n;
|
|
101
|
+
}
|
|
87
102
|
/**
|
|
88
103
|
* Build the {@link PoolBudgetConfig} from env with documented defaults.
|
|
89
104
|
* Field ↔ env name:
|
|
@@ -95,7 +110,27 @@ function parsePositiveInt(raw, fallback) {
|
|
|
95
110
|
* perAccountMax.shield_schema_svc ← POOL_MAX_SCHEMA_SVC
|
|
96
111
|
*/
|
|
97
112
|
function loadPoolBudget(env = process.env) {
|
|
113
|
+
const single = env.SHIELD_DB_LAYOUT === 'single';
|
|
114
|
+
const maxUser = single
|
|
115
|
+
? strictOptionalPoolInt(env.APP_DB_MAX_USER_CONNECTIONS, 'APP_DB_MAX_USER_CONNECTIONS')
|
|
116
|
+
: undefined;
|
|
98
117
|
return {
|
|
118
|
+
...(single
|
|
119
|
+
? {
|
|
120
|
+
layout: 'single',
|
|
121
|
+
// Same rule as the engine pool identity opens (`engineDbFromEnv`): POOL_MAX_ENGINE, else
|
|
122
|
+
// today's APP_DB_MAX_CONNECTIONS fallback, else 10. The budget must count the pool that
|
|
123
|
+
// really opens, so an unset POOL_MAX_ENGINE with a large APP_DB_MAX_CONNECTIONS fails the
|
|
124
|
+
// budget instead of under-counting it (spec AC-12).
|
|
125
|
+
// The numbers are parsed exactly as identity's pools parse them (strict: a set but
|
|
126
|
+
// invalid value throws naming the variable, never a lenient fallback).
|
|
127
|
+
engineMax: strictOptionalPoolInt(env.POOL_MAX_ENGINE, 'POOL_MAX_ENGINE') ??
|
|
128
|
+
strictOptionalPoolInt(env.APP_DB_MAX_CONNECTIONS, 'APP_DB_MAX_CONNECTIONS') ??
|
|
129
|
+
10,
|
|
130
|
+
authzMax: strictOptionalPoolInt(env.APP_AUTHZ_POOL_MAX, 'APP_AUTHZ_POOL_MAX') ?? 4,
|
|
131
|
+
...(maxUser !== undefined ? { maxUserConnections: maxUser } : {}),
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
99
134
|
workers: parsePositiveInt(env.POOL_WORKERS, exports.DEFAULT_POOL_BUDGET.workers),
|
|
100
135
|
maxConnections: parsePositiveInt(env.APP_DB_MAX_CONNECTIONS, exports.DEFAULT_POOL_BUDGET.maxConnections),
|
|
101
136
|
headroom: parsePositiveInt(env.POOL_HEADROOM, exports.DEFAULT_POOL_BUDGET.headroom),
|
|
@@ -166,6 +201,10 @@ function parseOptionalPositiveInt(raw) {
|
|
|
166
201
|
*/
|
|
167
202
|
function loadDbConfig(env = process.env, schemas = (0, core_1.resolveFrameworkSchemas)(env)) {
|
|
168
203
|
const database = parseString(env.APP_DB_NAME, schemas.identity);
|
|
204
|
+
// Single layout (spec §5.C): no `<ns>_core` account exists to default to; fail closed.
|
|
205
|
+
if (schemas.layout === 'single' && parseString(env.APP_DB_USER, '') === '') {
|
|
206
|
+
throw new core_1.FrameworkSchemaConfigError('APP_DB_USER is required when SHIELD_DB_LAYOUT=single (there is no default runtime account).');
|
|
207
|
+
}
|
|
169
208
|
const allowed = [schemas.app, schemas.identity, schemas.authz, schemas.audit];
|
|
170
209
|
if (!allowed.includes(database)) {
|
|
171
210
|
throw new core_1.FrameworkSchemaConfigError(`APP_DB_NAME must be one of the framework databases for namespace ` +
|
|
@@ -174,7 +213,9 @@ function loadDbConfig(env = process.env, schemas = (0, core_1.resolveFrameworkSc
|
|
|
174
213
|
return {
|
|
175
214
|
host: parseString(env.APP_DB_HOST, exports.DEFAULT_DB_CONFIG.host),
|
|
176
215
|
port: parsePort(env.APP_DB_PORT, exports.DEFAULT_DB_CONFIG.port),
|
|
177
|
-
user:
|
|
216
|
+
user: schemas.layout === 'single'
|
|
217
|
+
? parseString(env.APP_DB_USER, '')
|
|
218
|
+
: parseString(env.APP_DB_USER, (0, core_1.frameworkAccountName)(schemas.namespace, 'core')),
|
|
178
219
|
password: parseString(env.APP_DB_PASSWORD, ''),
|
|
179
220
|
database,
|
|
180
221
|
tls: loadDbTlsConfig(env),
|
|
@@ -437,18 +478,25 @@ function parseStorageMaxBytes(raw) {
|
|
|
437
478
|
* react (the boot assertion throws; tests inspect the fields).
|
|
438
479
|
*/
|
|
439
480
|
function evaluatePoolBudget(cfg) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
cfg.perAccountMax.
|
|
481
|
+
// Single layout (AC-12): one account opens core + engine + authz; app_crud/schema_svc are never opened.
|
|
482
|
+
const perWorkerConnections = cfg.layout === 'single'
|
|
483
|
+
? cfg.perAccountMax.shield_core + (cfg.engineMax ?? 10) + (cfg.authzMax ?? 4)
|
|
484
|
+
: cfg.perAccountMax.shield_core +
|
|
485
|
+
cfg.perAccountMax.shield_app_crud +
|
|
486
|
+
cfg.perAccountMax.shield_schema_svc;
|
|
443
487
|
const runtimeConnections = cfg.workers * perWorkerConnections;
|
|
444
488
|
const requiredConnections = runtimeConnections + cfg.headroom;
|
|
445
489
|
const slack = cfg.maxConnections - requiredConnections;
|
|
490
|
+
const withinUserBudget = cfg.layout !== 'single' ||
|
|
491
|
+
cfg.maxUserConnections === undefined ||
|
|
492
|
+
requiredConnections <= cfg.maxUserConnections;
|
|
446
493
|
return {
|
|
447
494
|
perWorkerConnections,
|
|
448
495
|
runtimeConnections,
|
|
449
496
|
requiredConnections,
|
|
450
497
|
slack,
|
|
451
|
-
withinBudget: requiredConnections <= cfg.maxConnections,
|
|
452
|
-
schemaSvcWithinHardCap: cfg.perAccountMax.shield_schema_svc <= exports.SCHEMA_SVC_POOL_HARD_CAP,
|
|
498
|
+
withinBudget: requiredConnections <= cfg.maxConnections && withinUserBudget,
|
|
499
|
+
schemaSvcWithinHardCap: cfg.layout === 'single' || cfg.perAccountMax.shield_schema_svc <= exports.SCHEMA_SVC_POOL_HARD_CAP,
|
|
500
|
+
withinUserBudget,
|
|
453
501
|
};
|
|
454
502
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Astram framework — the enforcement guard + real-world adapters (the 'hands'). One global deny-by-default gate every request passes through (ADR-0002), classification decorators, boot-pool assertion (ADR-0018), the forRoot() registration seam, and the DB/Redis/crypto adapters that connect the sealed core to a real backend. Depends on @astram/core (ADR-0003, ADR-0023).",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"argon2": "^0.41.1",
|
|
37
37
|
"ioredis": "^5.4.1",
|
|
38
38
|
"mariadb": "^3.5.4",
|
|
39
|
-
"@astram/core": "1.
|
|
40
|
-
"@astram/engine": "0.
|
|
39
|
+
"@astram/core": "1.8.0",
|
|
40
|
+
"@astram/engine": "0.9.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@nestjs/common": "^11",
|