@astram/guard 0.14.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 +76 -31
- package/dist/authentication/mariadb-api-key-store.d.ts +10 -1
- package/dist/authentication/mariadb-api-key-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-api-key-store.js +20 -10
- package/dist/authentication/mariadb-credential-resolver.d.ts +8 -1
- package/dist/authentication/mariadb-credential-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-resolver.js +15 -5
- package/dist/authentication/mariadb-credential-writer.d.ts +6 -1
- package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-credential-writer.js +9 -3
- package/dist/authentication/mariadb-email-address-changer.d.ts +10 -1
- package/dist/authentication/mariadb-email-address-changer.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-address-changer.js +22 -9
- package/dist/authentication/mariadb-email-change-token-store.d.ts +8 -1
- package/dist/authentication/mariadb-email-change-token-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-email-change-token-store.js +18 -9
- package/dist/authentication/mariadb-mfa-device-store.d.ts +6 -1
- package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-mfa-device-store.js +22 -13
- package/dist/authentication/mariadb-profile-resolver.d.ts +6 -1
- package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-resolver.js +12 -6
- package/dist/authentication/mariadb-profile-writer.d.ts +6 -1
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -1
- package/dist/authentication/mariadb-profile-writer.js +11 -4
- package/dist/authentication/mariadb-recovery-code-store.d.ts +6 -1
- package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-recovery-code-store.js +11 -5
- package/dist/authentication/mariadb-reset-token-store.d.ts +6 -1
- package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -1
- package/dist/authentication/mariadb-reset-token-store.js +11 -5
- package/dist/authentication/mariadb-session-sweeper.d.ts +6 -1
- package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -1
- package/dist/authentication/mariadb-session-sweeper.js +11 -5
- package/dist/authz/cross-tenant-grant-writer.d.ts.map +1 -1
- package/dist/authz/cross-tenant-grant-writer.js +4 -3
- package/dist/authz/list-scope-sql.d.ts +10 -2
- package/dist/authz/list-scope-sql.d.ts.map +1 -1
- package/dist/authz/list-scope-sql.js +4 -4
- package/dist/authz/mariadb-cross-tenant-grant-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-cross-tenant-grant-adapter.js +4 -3
- package/dist/authz/mariadb-list-scope-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-list-scope-adapter.js +4 -3
- package/dist/authz/mariadb-policy-store.d.ts.map +1 -1
- package/dist/authz/mariadb-policy-store.js +4 -3
- package/dist/authz/mariadb-record-rule-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-rule-adapter.js +4 -3
- package/dist/authz/mariadb-record-share-adapter.d.ts.map +1 -1
- package/dist/authz/mariadb-record-share-adapter.js +4 -3
- package/dist/authz/record-share-cascade.d.ts.map +1 -1
- package/dist/authz/record-share-cascade.js +4 -3
- package/dist/authz/record-share-writer.d.ts.map +1 -1
- package/dist/authz/record-share-writer.js +4 -3
- package/dist/authz/wire-record-authorization.d.ts +8 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +14 -6
- package/dist/boot/boot-sequence.d.ts +10 -0
- package/dist/boot/boot-sequence.d.ts.map +1 -1
- package/dist/boot/boot-sequence.js +47 -3
- package/dist/boot/migration-ledger-check.d.ts +94 -0
- package/dist/boot/migration-ledger-check.d.ts.map +1 -0
- package/dist/boot/migration-ledger-check.js +280 -0
- 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 +46 -3
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js +90 -11
- package/dist/config/config.module.d.ts +7 -0
- package/dist/config/config.module.d.ts.map +1 -1
- package/dist/config/config.module.js +8 -1
- package/dist/config/framework-schema-names.d.ts +29 -0
- package/dist/config/framework-schema-names.d.ts.map +1 -0
- package/dist/config/framework-schema-names.js +41 -0
- package/dist/identity/permission-facts.adapter.d.ts +7 -1
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -1
- package/dist/identity/permission-facts.adapter.js +15 -6
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -2
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +43 -7
- package/dist/session/mariadb-session-record-store.d.ts +8 -1
- package/dist/session/mariadb-session-record-store.d.ts.map +1 -1
- package/dist/session/mariadb-session-record-store.js +23 -13
- package/dist/session/session-restriction-store.d.ts +8 -1
- package/dist/session/session-restriction-store.d.ts.map +1 -1
- package/dist/session/session-restriction-store.js +15 -5
- package/migrations/1710000000006-CreateAuthzSchema.cjs +15 -4
- package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +11 -3
- package/migrations/1710000000008-CreateAccessBundleTables.cjs +14 -4
- package/migrations/1710000000009-CreateRecordPermissionRule.cjs +15 -4
- package/migrations/1710000000014-CreateRecordShare.cjs +15 -4
- package/migrations/1710000000015-CreateScopeClosure.cjs +14 -4
- package/migrations/1710000000016-AlterCompanyParentFkRestrict.cjs +20 -9
- package/migrations/1710000000017-CreateServiceAccountKey.cjs +14 -4
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-ledger-check.d.ts","sourceRoot":"","sources":["../../src/boot/migration-ledger-check.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAKL,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC7B,MAAM,cAAc,CAAC;AAGtB,yEAAyE;AACzE,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE5D,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,YAAY,EAAE,uBAAuB,EAAE,CAAC;AAExC,MAAM,WAAW,+BAAgC,SAAQ,uBAAuB;IAC9E;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC3D,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IACnD,0FAA0F;IAC1F,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC;AAED,MAAM,WAAW,8BAA8B;IAC7C,qDAAqD;IACrD,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,iGAAiG;IACjG,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAgBD,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAO3D;AAWD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,OAAO,EAAE,gBAAgB,GACxB,uBAAuB,CAEzB;AAED,oFAAoF;AACpF,MAAM,WAAW,4BAA6B,SAAQ,uBAAuB;IAC3E,QAAQ,CAAC,YAAY,EAAE,SAAS,uBAAuB,EAAE,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,OAAO,EAAE,gBAAgB,GACxB,4BAA4B,CAG9B;AA+CD;;;;;GAKG;AACH,wBAAsB,gCAAgC,CACpD,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,8BAA8B,CAAC,CAyFzC;AAED,4FAA4F;AAC5F,eAAO,MAAM,gCAAgC,eAA6C,CAAC;AAE3F,qGAAqG;AACrG,eAAO,MAAM,mCAAmC,eAAgD,CAAC;AAEjG;;;;;GAKG;AACH,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAOpB;IAEJ,iFAAiF;IACjF,OAAO,CAAC,MAAM,CAAC,GAAG;IAQlB,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,+BAA+B,GAAG,IAAI;IAexD,iFAAiF;IACjF,GAAG,CACD,QAAQ,EAAE,uBAAuB,GAAG;QAAE,YAAY,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAA;KAAE,GACxF,OAAO,CAAC,8BAA8B,CAAC;CAY3C;AAED;;;;;GAKG;AACH,qBAAa,6BAA8B,YAAW,sBAAsB;IAExE,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,OAAO,EAAE,+BAA+B,EACxC,QAAQ,GAAE,uBAAuB,GAAG,IAAW;IAK5D,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;CAI9C"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FrameworkMigrationLedgerCheck = exports.MigrationLedgerRegistry = exports.SHIELD_GUARD_MIGRATION_LEDGER_CHECK = exports.SHIELD_MIGRATION_LEDGER_REGISTRY = void 0;
|
|
4
|
+
exports.frameworkMigrationIds = frameworkMigrationIds;
|
|
5
|
+
exports.resolveMigrationLedger = resolveMigrationLedger;
|
|
6
|
+
exports.resolveMigrationLedgerCheck = resolveMigrationLedgerCheck;
|
|
7
|
+
exports.assertFrameworkMigrationsApplied = assertFrameworkMigrationsApplied;
|
|
8
|
+
/**
|
|
9
|
+
* Boot check: every framework migration a package ships is recorded in the `@astram/migrate`
|
|
10
|
+
* ledger (configurable-framework-db spec 5.D, §13).
|
|
11
|
+
*
|
|
12
|
+
* The ledger lives where `astram-migrate` writes it (spec 5.G): `<ns>_app` when
|
|
13
|
+
* `SHIELD_DB_NAMESPACE` is set, else `DDL_DB_NAME ?? APP_DB_NAME ?? <default app database>` of
|
|
14
|
+
* the migrate job's env; the table is `APP_MIGRATION_TABLE ?? 'schema_migrations'`. With the
|
|
15
|
+
* namespace set the check reads `<ns>_app` directly. In default mode it cannot know the migrate
|
|
16
|
+
* job's env, so it probes core `migrationLedgerCandidates` in order (`DDL_DB_NAME`, the runtime
|
|
17
|
+
* `APP_DB_NAME`, `shield_app`; one information_schema probe each, at bootstrap only) and reads the
|
|
18
|
+
* first ledger table that exists (G-007 QA P1). That ledger must hold every id; none found fails
|
|
19
|
+
* closed naming every location tried.
|
|
20
|
+
*
|
|
21
|
+
* Missing ids, no ledger table, or an unreadable ledger:
|
|
22
|
+
* - outside `NODE_ENV` development/test (unset included) ⇒ throws, boot fails (Shield #1);
|
|
23
|
+
* - in development/test ⇒ one warning, boot continues.
|
|
24
|
+
* An unreadable ledger is never treated as "all applied". Messages carry the database name, the
|
|
25
|
+
* table name, migration ids and a driver error code only: never a user, host, password or the
|
|
26
|
+
* driver's message text (core 70-secrets).
|
|
27
|
+
*
|
|
28
|
+
* Runs once, at application bootstrap, never per request (Shield #12). Who checks what:
|
|
29
|
+
* - `AuthModule` (`auth: true`) checks `guardMigrationsDir` over its runtime pool;
|
|
30
|
+
* - `@astram/identity` checks `guardMigrationsDir` AND `identityMigrationsDir` over its own pool
|
|
31
|
+
* (identity reads guard's authz tables; guard never imports identity, ADR-0003);
|
|
32
|
+
* - an enforcement-only app (no `auth`, no identity) opens no DB pool and reads no framework
|
|
33
|
+
* table, so it checks nothing.
|
|
34
|
+
* When both run in one app, the per-app {@link MigrationLedgerRegistry} (provided by
|
|
35
|
+
* `ShieldModule`) merges them: every hook registers its directories when constructed, and the
|
|
36
|
+
* first hook to bootstrap runs ONE SELECT per ledger location over the union; the others await
|
|
37
|
+
* that same result (a failure fails every one of them). Without the registry each hook runs alone.
|
|
38
|
+
*/
|
|
39
|
+
const node_fs_1 = require("node:fs");
|
|
40
|
+
const core_1 = require("@astram/core");
|
|
41
|
+
const app_config_js_1 = require("../config/app-config.js");
|
|
42
|
+
/** At most this many ids are listed in the message (spec §13). */
|
|
43
|
+
const MAX_LISTED_IDS = 10;
|
|
44
|
+
/** Same id rule as `@astram/migrate`'s loader, restricted to the shipped `.cjs` files. */
|
|
45
|
+
const MIGRATION_FILE = /^(\d+)-.+\.cjs$/;
|
|
46
|
+
/** MariaDB "table doesn't exist". */
|
|
47
|
+
const ER_NO_SUCH_TABLE = 1146;
|
|
48
|
+
/** `1`, `01` and `001` are the same migration id (migrate orders ids numerically). */
|
|
49
|
+
function idKey(id) {
|
|
50
|
+
return id.replace(/^0+(?=\d)/, '');
|
|
51
|
+
}
|
|
52
|
+
/** The migration ids shipped in `dir` (`.cjs` files only), ascending. */
|
|
53
|
+
function frameworkMigrationIds(dir) {
|
|
54
|
+
const ids = [];
|
|
55
|
+
for (const file of (0, node_fs_1.readdirSync)(dir)) {
|
|
56
|
+
const m = MIGRATION_FILE.exec(file);
|
|
57
|
+
if (m)
|
|
58
|
+
ids.push(m[1]);
|
|
59
|
+
}
|
|
60
|
+
return sortIds(ids);
|
|
61
|
+
}
|
|
62
|
+
/** Ascending numeric order over decimal id strings. */
|
|
63
|
+
function sortIds(ids) {
|
|
64
|
+
return ids.sort((a, b) => {
|
|
65
|
+
const ka = idKey(a);
|
|
66
|
+
const kb = idKey(b);
|
|
67
|
+
return ka.length - kb.length || (ka < kb ? -1 : ka > kb ? 1 : 0);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The migrate CLI's ledger location rule (core `resolveMigrationLedgerLocation`: `<ns>_app` when
|
|
72
|
+
* namespaced, else `DDL_DB_NAME ?? <default app>`). The boot check itself uses
|
|
73
|
+
* {@link resolveMigrationLedgerCheck}, which also tries the runtime `APP_DB_NAME`.
|
|
74
|
+
* @throws FrameworkSchemaConfigError on a conflicting `DDL_DB_NAME` or invalid names.
|
|
75
|
+
*/
|
|
76
|
+
function resolveMigrationLedger(env, schemas) {
|
|
77
|
+
return (0, core_1.resolveMigrationLedgerLocation)(schemas, env);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The boot check's ledger locations for this process's env (G-007 QA P1): core
|
|
81
|
+
* `migrationLedgerCandidates`, split into the first location and its `alternatives`. Namespace
|
|
82
|
+
* set ⇒ `<ns>_app` only; default ⇒ `DDL_DB_NAME`, the runtime `APP_DB_NAME`, `shield_app`.
|
|
83
|
+
* Spread it into {@link FrameworkMigrationsCheckOptions}.
|
|
84
|
+
* @throws FrameworkSchemaConfigError on a conflicting `DDL_DB_NAME` or invalid names.
|
|
85
|
+
*/
|
|
86
|
+
function resolveMigrationLedgerCheck(env, schemas) {
|
|
87
|
+
const [first, ...alternatives] = (0, core_1.migrationLedgerCandidates)(schemas, env);
|
|
88
|
+
return { database: first.database, table: first.table, alternatives };
|
|
89
|
+
}
|
|
90
|
+
/** The driver error code only (never its message: it can carry user@host). */
|
|
91
|
+
function errorCode(err) {
|
|
92
|
+
if (typeof err === 'object' && err !== null) {
|
|
93
|
+
const { code, errno } = err;
|
|
94
|
+
if (typeof code === 'string' && /^[A-Z0-9_]{1,64}$/.test(code))
|
|
95
|
+
return code;
|
|
96
|
+
if (typeof errno === 'number')
|
|
97
|
+
return `errno ${errno}`;
|
|
98
|
+
}
|
|
99
|
+
return 'unknown error';
|
|
100
|
+
}
|
|
101
|
+
function isNoSuchTable(err) {
|
|
102
|
+
if (typeof err !== 'object' || err === null)
|
|
103
|
+
return false;
|
|
104
|
+
const { code, errno } = err;
|
|
105
|
+
return code === 'ER_NO_SUCH_TABLE' || errno === ER_NO_SUCH_TABLE;
|
|
106
|
+
}
|
|
107
|
+
/** No candidate holds a ledger table: name every location, tell the operator what to set. */
|
|
108
|
+
function notFoundMessage(tried, unverified) {
|
|
109
|
+
const where = tried.map((l) => `${l.database}.${l.table}`).join(', ');
|
|
110
|
+
return (`Shield migration ledger not found (tried, in order: ${where}); ` +
|
|
111
|
+
`${unverified} framework migrations are unverified. Set DDL_DB_NAME to the ledger database, ` +
|
|
112
|
+
'the same value for `astram-migrate` and the server, and run `astram-migrate migrate` ' +
|
|
113
|
+
'with the deploy account before starting the server.');
|
|
114
|
+
}
|
|
115
|
+
/** One information_schema probe: does `l.database` hold the table `l.table`? Names validated. */
|
|
116
|
+
function probeSql(l) {
|
|
117
|
+
return ('SELECT 1 AS present FROM information_schema.TABLES ' +
|
|
118
|
+
`WHERE TABLE_SCHEMA = '${l.database}' AND TABLE_NAME = '${l.table}' LIMIT 1`);
|
|
119
|
+
}
|
|
120
|
+
function missingMessage(database, missing) {
|
|
121
|
+
const listed = missing.slice(0, MAX_LISTED_IDS);
|
|
122
|
+
const more = missing.length - listed.length;
|
|
123
|
+
const ids = more > 0 ? `${listed.join(', ')}, … ${more} more` : listed.join(', ');
|
|
124
|
+
return (`Shield database ${database} is missing ${missing.length} framework migrations (${ids}). ` +
|
|
125
|
+
'Run `astram-migrate migrate` with the deploy account before starting the server.');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Compare the shipped migration ids in `migrationsDir` with the ledger; throw (production) or
|
|
129
|
+
* warn (development/test) when any is missing or the ledger cannot be read.
|
|
130
|
+
* @throws Error naming the database and up to 10 missing ids, outside development/test; and in
|
|
131
|
+
* every environment for an invalid ledger database or table name (checked before any query).
|
|
132
|
+
*/
|
|
133
|
+
async function assertFrameworkMigrationsApplied(options) {
|
|
134
|
+
// Every location is validated before any query (operator names: any plain MariaDB identifier).
|
|
135
|
+
const locations = [
|
|
136
|
+
{ database: options.database, table: options.table },
|
|
137
|
+
...(options.alternatives ?? []),
|
|
138
|
+
].map((l) => {
|
|
139
|
+
const database = (0, core_1.assertLedgerDatabaseName)(l.database, 'migration ledger database');
|
|
140
|
+
if (typeof l.table !== 'string' || !core_1.MIGRATION_LEDGER_TABLE_PATTERN.test(l.table)) {
|
|
141
|
+
throw new Error(`APP_MIGRATION_TABLE is not a valid table name; expected ${core_1.MIGRATION_LEDGER_TABLE_PATTERN.source}`);
|
|
142
|
+
}
|
|
143
|
+
return { database, table: l.table };
|
|
144
|
+
});
|
|
145
|
+
let { database, table } = locations[0];
|
|
146
|
+
const devOrTest = (0, app_config_js_1.isDevOrTestNodeEnv)(options.nodeEnv);
|
|
147
|
+
const fail = (message, missing) => {
|
|
148
|
+
if (!devOrTest)
|
|
149
|
+
throw new Error(message);
|
|
150
|
+
options.logger.warn(`${message} (NODE_ENV=${options.nodeEnv}: continuing)`);
|
|
151
|
+
return { ok: false, missing };
|
|
152
|
+
};
|
|
153
|
+
let shipped;
|
|
154
|
+
try {
|
|
155
|
+
const dirs = typeof options.migrationsDir === 'string' ? [options.migrationsDir] : options.migrationsDir;
|
|
156
|
+
shipped = sortIds([...new Set(dirs.flatMap((d) => frameworkMigrationIds(d)))]);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
return fail(`Shield database ${database}: cannot read the framework migrations directory ` +
|
|
160
|
+
`(${errorCode(err)}); the package install is incomplete.`, []);
|
|
161
|
+
}
|
|
162
|
+
if (shipped.length === 0)
|
|
163
|
+
return { ok: true, missing: [] };
|
|
164
|
+
// Several candidates (default namespace): use the first whose ledger table exists. A probe
|
|
165
|
+
// error is never "not here" or "all applied": it fails like an unreadable ledger.
|
|
166
|
+
if (locations.length > 1) {
|
|
167
|
+
let found;
|
|
168
|
+
for (const l of locations) {
|
|
169
|
+
let rows;
|
|
170
|
+
try {
|
|
171
|
+
rows = await options.query(probeSql(l));
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
return fail(`Shield database ${l.database}: cannot look for the migration ledger ` +
|
|
175
|
+
`${l.database}.${l.table} (${errorCode(err)}); ${shipped.length} framework ` +
|
|
176
|
+
'migrations are unverified.', shipped);
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(rows)) {
|
|
179
|
+
return fail(`Shield database ${l.database}: cannot look for the migration ledger ` +
|
|
180
|
+
`${l.database}.${l.table} (UNEXPECTED_RESULT); ${shipped.length} framework ` +
|
|
181
|
+
'migrations are unverified.', shipped);
|
|
182
|
+
}
|
|
183
|
+
if (rows.length > 0) {
|
|
184
|
+
found = l;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (found === undefined)
|
|
189
|
+
return fail(notFoundMessage(locations, shipped.length), shipped);
|
|
190
|
+
({ database, table } = found);
|
|
191
|
+
}
|
|
192
|
+
let applied;
|
|
193
|
+
try {
|
|
194
|
+
const rows = await options.query(`SELECT id FROM \`${database}\`.\`${table}\``);
|
|
195
|
+
if (!Array.isArray(rows))
|
|
196
|
+
throw Object.assign(new Error(), { code: 'UNEXPECTED_RESULT' });
|
|
197
|
+
applied = new Set(rows.map((r) => idKey(String(r?.id ?? ''))));
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
// No ledger table: nothing was ever migrated here, so every shipped id is missing.
|
|
201
|
+
if (isNoSuchTable(err))
|
|
202
|
+
return fail(missingMessage(database, shipped), shipped);
|
|
203
|
+
return fail(`Shield database ${database}: cannot read the migration ledger ${database}.${table} ` +
|
|
204
|
+
`(${errorCode(err)}); ${shipped.length} framework migrations are unverified.`, shipped);
|
|
205
|
+
}
|
|
206
|
+
const missing = shipped.filter((id) => !applied.has(idKey(id)));
|
|
207
|
+
if (missing.length === 0)
|
|
208
|
+
return { ok: true, missing: [] };
|
|
209
|
+
return fail(missingMessage(database, missing), missing);
|
|
210
|
+
}
|
|
211
|
+
/** DI token of the per-app {@link MigrationLedgerRegistry} (provided by `ShieldModule`). */
|
|
212
|
+
exports.SHIELD_MIGRATION_LEDGER_REGISTRY = Symbol('SHIELD_MIGRATION_LEDGER_REGISTRY');
|
|
213
|
+
/** DI token of guard's own check (`AuthModule`); identity provides its check under its own token. */
|
|
214
|
+
exports.SHIELD_GUARD_MIGRATION_LEDGER_CHECK = Symbol('SHIELD_GUARD_MIGRATION_LEDGER_CHECK');
|
|
215
|
+
/**
|
|
216
|
+
* Merges the ledger checks of one application (guard's `AuthModule` and identity) so each ledger
|
|
217
|
+
* location is read once. Hooks {@link register} at construction (all providers exist before any
|
|
218
|
+
* bootstrap hook runs); the first {@link run} for a location checks the union of every registered
|
|
219
|
+
* directory and caches that promise, so later hooks share the result, rejection included.
|
|
220
|
+
*/
|
|
221
|
+
class MigrationLedgerRegistry {
|
|
222
|
+
entries = new Map();
|
|
223
|
+
/** The whole candidate list: hooks with the same list share one probe + read. */
|
|
224
|
+
static key(l) {
|
|
225
|
+
return [l, ...(l.alternatives ?? [])]
|
|
226
|
+
.map((x) => `${x.database}\u0000${x.table}`)
|
|
227
|
+
.join('\u0001');
|
|
228
|
+
}
|
|
229
|
+
/** Add `options.migrationsDir` to the check of `options`' ledger location. */
|
|
230
|
+
register(options) {
|
|
231
|
+
const dirs = typeof options.migrationsDir === 'string' ? [options.migrationsDir] : options.migrationsDir;
|
|
232
|
+
const key = MigrationLedgerRegistry.key(options);
|
|
233
|
+
const entry = this.entries.get(key);
|
|
234
|
+
if (entry === undefined) {
|
|
235
|
+
this.entries.set(key, { options, dirs: new Set(dirs) });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (entry.result !== undefined) {
|
|
239
|
+
throw new Error('MigrationLedgerRegistry: a check registered after the ledger was read.');
|
|
240
|
+
}
|
|
241
|
+
for (const d of dirs)
|
|
242
|
+
entry.dirs.add(d);
|
|
243
|
+
}
|
|
244
|
+
/** The (shared) check of `location`'s ledger over every registered directory. */
|
|
245
|
+
run(location) {
|
|
246
|
+
const entry = this.entries.get(MigrationLedgerRegistry.key(location));
|
|
247
|
+
// Never registered: nothing to share, nothing claims "checked" (fail closed).
|
|
248
|
+
if (entry === undefined) {
|
|
249
|
+
return Promise.reject(new Error('MigrationLedgerRegistry: ledger location not registered.'));
|
|
250
|
+
}
|
|
251
|
+
entry.result ??= assertFrameworkMigrationsApplied({
|
|
252
|
+
...entry.options,
|
|
253
|
+
migrationsDir: [...entry.dirs],
|
|
254
|
+
});
|
|
255
|
+
return entry.result;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
exports.MigrationLedgerRegistry = MigrationLedgerRegistry;
|
|
259
|
+
/**
|
|
260
|
+
* The boot hook: runs {@link assertFrameworkMigrationsApplied} once, when Nest bootstraps the
|
|
261
|
+
* application (after module init, before the HTTP server listens). Never on a request. With a
|
|
262
|
+
* {@link MigrationLedgerRegistry}, it shares one read per ledger location with the app's other
|
|
263
|
+
* hooks; without one (`null`/absent), it checks alone.
|
|
264
|
+
*/
|
|
265
|
+
class FrameworkMigrationLedgerCheck {
|
|
266
|
+
options;
|
|
267
|
+
registry;
|
|
268
|
+
constructor(options, registry = null) {
|
|
269
|
+
this.options = options;
|
|
270
|
+
this.registry = registry;
|
|
271
|
+
registry?.register(options);
|
|
272
|
+
}
|
|
273
|
+
async onApplicationBootstrap() {
|
|
274
|
+
if (this.registry)
|
|
275
|
+
await this.registry.run(this.options);
|
|
276
|
+
else
|
|
277
|
+
await assertFrameworkMigrationsApplied(this.options);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.FrameworkMigrationLedgerCheck = FrameworkMigrationLedgerCheck;
|
|
@@ -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"}
|