@astram/guard 0.15.0 → 0.17.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/authentication/argon2id-password-hasher.d.ts +3 -0
- package/dist/authentication/argon2id-password-hasher.d.ts.map +1 -1
- package/dist/authentication/argon2id-password-hasher.js +21 -0
- package/dist/authentication/credential-provisioner.d.ts +30 -0
- package/dist/authentication/credential-provisioner.d.ts.map +1 -0
- package/dist/authentication/credential-provisioner.js +16 -0
- package/dist/authentication/mariadb-credential-provisioner.d.ts +26 -0
- package/dist/authentication/mariadb-credential-provisioner.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-provisioner.js +72 -0
- package/dist/authz/operation-marker-check.d.ts +14 -0
- package/dist/authz/operation-marker-check.d.ts.map +1 -0
- package/dist/authz/operation-marker-check.js +25 -0
- package/dist/authz/wire-record-authorization.d.ts.map +1 -1
- package/dist/authz/wire-record-authorization.js +3 -2
- 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/dist/config/reserved-principal-ids.d.ts +14 -0
- package/dist/config/reserved-principal-ids.d.ts.map +1 -0
- package/dist/config/reserved-principal-ids.js +24 -0
- package/dist/config/root-ids-from-env.d.ts +2 -0
- package/dist/config/root-ids-from-env.d.ts.map +1 -0
- package/dist/config/root-ids-from-env.js +41 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -1
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +4 -0
- 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DG;AAEH,OAAO,EAGL,KAAK,aAAa,EAElB,KAAK,QAAQ,EACd,MAAM,gBAAgB,CAAC;AAaxB,OAAO,KAAK,EAGV,kBAAkB,EAOlB,QAAQ,EACR,cAAc,EAIf,MAAM,cAAc,CAAC;AA8GtB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAChF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAGpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAavD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QACqE,CAAC;AAE7F;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,EAC3C,eAAe,EAAE,cAAc,GAC9B,cAAc,CAEhB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,cAAc,CAAC,CAGvE;AAsPD,wFAAwF;AACxF,eAAO,MAAM,0BAA0B,eAAuC,CAAC;AAE/E;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAWvC,CAAC;AAyUF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAC7C,QAAQ,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC;IAC3C;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,GAAG,UAAU,CAAC;IAChD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,cAAc,CAAC;IACvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;oGAEgG;IAChG,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,uGAAuG;IACvG,QAAQ,CAAC,wBAAwB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtD;;;;;;;OAOG;IACH,QAAQ,CAAC,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IACvD;;;;;;;;OAQG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IACrD;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IAC/C;;;;;;OAMG;IACH,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CAC1C;AA6ID;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,CAcrF;AAmGD;;;;;;GAMG;AACH,qBACa,UAAU;IACrB;;;;;OAKG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa;CAgL/D"}
|
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,
|
|
@@ -29,6 +29,9 @@ export interface Argon2idParams {
|
|
|
29
29
|
readonly timeCost?: number;
|
|
30
30
|
readonly parallelism?: number;
|
|
31
31
|
}
|
|
32
|
+
/** `!locked:` + a real argon2id hash of 32 random bytes that are never kept. */
|
|
33
|
+
export declare function lockedPasswordHash(): Promise<string>;
|
|
34
|
+
export declare function isLockedHash(hash: string): boolean;
|
|
32
35
|
/** The DI token consumers/ShieldModule bind the concrete hasher under. */
|
|
33
36
|
export declare const SHIELD_PASSWORD_HASHER: unique symbol;
|
|
34
37
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"argon2id-password-hasher.d.ts","sourceRoot":"","sources":["../../src/authentication/argon2id-password-hasher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"argon2id-password-hasher.d.ts","sourceRoot":"","sources":["../../src/authentication/argon2id-password-hasher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAWD,gFAAgF;AAChF,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAM1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,eAAmC,CAAC;AAEvE;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,cAAc;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;gBAEnD,MAAM,GAAE,cAAmB;IASvC,8EAA8E;IACxE,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK9C;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAahE;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,GAAE,cAAmB,GAAG;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,sBAAsB,CAAC;CAClC,CAKA"}
|
|
@@ -49,13 +49,29 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
49
49
|
})();
|
|
50
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
51
|
exports.Argon2idPasswordHasher = exports.SHIELD_PASSWORD_HASHER = void 0;
|
|
52
|
+
exports.lockedPasswordHash = lockedPasswordHash;
|
|
53
|
+
exports.isLockedHash = isLockedHash;
|
|
52
54
|
exports.argon2idHasherProvider = argon2idHasherProvider;
|
|
55
|
+
const node_crypto_1 = require("node:crypto");
|
|
53
56
|
const argon2 = __importStar(require("argon2"));
|
|
54
57
|
const DEFAULT_PARAMS = {
|
|
55
58
|
memoryCost: 19_456,
|
|
56
59
|
timeCost: 2,
|
|
57
60
|
parallelism: 1,
|
|
58
61
|
};
|
|
62
|
+
/** Prefix marking a credential nobody can authenticate against (set later via reset). */
|
|
63
|
+
const LOCKED_PREFIX = '!locked:';
|
|
64
|
+
/** `!locked:` + a real argon2id hash of 32 random bytes that are never kept. */
|
|
65
|
+
async function lockedPasswordHash() {
|
|
66
|
+
const hash = await argon2.hash((0, node_crypto_1.randomBytes)(32), {
|
|
67
|
+
type: argon2.argon2id,
|
|
68
|
+
...DEFAULT_PARAMS,
|
|
69
|
+
});
|
|
70
|
+
return LOCKED_PREFIX + hash;
|
|
71
|
+
}
|
|
72
|
+
function isLockedHash(hash) {
|
|
73
|
+
return hash.startsWith(LOCKED_PREFIX);
|
|
74
|
+
}
|
|
59
75
|
/** The DI token consumers/ShieldModule bind the concrete hasher under. */
|
|
60
76
|
exports.SHIELD_PASSWORD_HASHER = Symbol('SHIELD_PASSWORD_HASHER');
|
|
61
77
|
/**
|
|
@@ -85,6 +101,11 @@ class Argon2idPasswordHasher {
|
|
|
85
101
|
*/
|
|
86
102
|
async verify(hash, plaintext) {
|
|
87
103
|
try {
|
|
104
|
+
if (isLockedHash(hash)) {
|
|
105
|
+
// Real verify (no timing shortcut) on the remainder, then false regardless of the outcome.
|
|
106
|
+
await argon2.verify(hash.slice(LOCKED_PREFIX.length), plaintext);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
88
109
|
return await argon2.verify(hash, plaintext);
|
|
89
110
|
}
|
|
90
111
|
catch {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CredentialProvisioner — the INSERT-ONLY port that creates a login credential for a new principal
|
|
3
|
+
* (org bootstrap). It never updates or replaces an existing credential: a principal that already has
|
|
4
|
+
* one is refused with {@link CredentialExistsError}, so provisioning can never reset someone's password.
|
|
5
|
+
*/
|
|
6
|
+
/** Caller-supplied SQL runner (a transaction connection's `query`); absent ⇒ the pool. */
|
|
7
|
+
export type CredentialQuery = (sql: string, params?: unknown[]) => Promise<unknown>;
|
|
8
|
+
export interface CredentialProvisionInput {
|
|
9
|
+
readonly principalId: string;
|
|
10
|
+
/** Login key (`ic_identity_hint`, the canonical email). */
|
|
11
|
+
readonly hint: string;
|
|
12
|
+
readonly tenantId: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PasswordCredentialProvisionInput extends CredentialProvisionInput {
|
|
15
|
+
readonly password: string;
|
|
16
|
+
}
|
|
17
|
+
/** `none` = no credential row; `locked` = unusable placeholder hash; `usable` = a real password hash. */
|
|
18
|
+
export type CredentialState = 'none' | 'locked' | 'usable';
|
|
19
|
+
export interface CredentialProvisioner {
|
|
20
|
+
/** Insert a credential no password verifies against (set later via reset). */
|
|
21
|
+
provisionLocked(input: CredentialProvisionInput, query?: CredentialQuery): Promise<void>;
|
|
22
|
+
/** Insert an Argon2id credential for `password` (≥ 8 chars). */
|
|
23
|
+
provisionPassword(input: PasswordCredentialProvisionInput, query?: CredentialQuery): Promise<void>;
|
|
24
|
+
stateOf(principalId: string, query?: CredentialQuery): Promise<CredentialState>;
|
|
25
|
+
}
|
|
26
|
+
/** The principal or login hint already has a credential. Fixed message: no hint, no driver text. */
|
|
27
|
+
export declare class CredentialExistsError extends Error {
|
|
28
|
+
constructor();
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=credential-provisioner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-provisioner.d.ts","sourceRoot":"","sources":["../../src/authentication/credential-provisioner.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,0FAA0F;AAC1F,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gCAAiC,SAAQ,wBAAwB;IAChF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,yGAAyG;AACzG,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3D,MAAM,WAAW,qBAAqB;IACpC,8EAA8E;IAC9E,eAAe,CAAC,KAAK,EAAE,wBAAwB,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,gEAAgE;IAChE,iBAAiB,CACf,KAAK,EAAE,gCAAgC,EACvC,KAAK,CAAC,EAAE,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACjF;AAED,oGAAoG;AACpG,qBAAa,qBAAsB,SAAQ,KAAK;;CAK/C"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CredentialProvisioner — the INSERT-ONLY port that creates a login credential for a new principal
|
|
4
|
+
* (org bootstrap). It never updates or replaces an existing credential: a principal that already has
|
|
5
|
+
* one is refused with {@link CredentialExistsError}, so provisioning can never reset someone's password.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.CredentialExistsError = void 0;
|
|
9
|
+
/** The principal or login hint already has a credential. Fixed message: no hint, no driver text. */
|
|
10
|
+
class CredentialExistsError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super('A credential already exists for this principal or login');
|
|
13
|
+
this.name = 'CredentialExistsError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.CredentialExistsError = CredentialExistsError;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MariaDbCredentialProvisioner — the framework CredentialProvisioner over `identity_credential`.
|
|
3
|
+
* INSERT only (never UPDATE/upsert): an existing principal, or a duplicate login hint (PK), is a
|
|
4
|
+
* {@link CredentialExistsError}. Stores only an Argon2id hash (or the locked placeholder); never logs it.
|
|
5
|
+
* Pass `query` to run inside the caller's transaction connection.
|
|
6
|
+
*/
|
|
7
|
+
import type { Pool } from 'mariadb';
|
|
8
|
+
import { type CredentialProvisioner, type CredentialProvisionInput, type CredentialQuery, type CredentialState, type PasswordCredentialProvisionInput } from './credential-provisioner.js';
|
|
9
|
+
import { type FrameworkSchemaNames } from '../config/framework-schema-names.js';
|
|
10
|
+
export declare class MariaDbCredentialProvisioner implements CredentialProvisioner {
|
|
11
|
+
private readonly pool;
|
|
12
|
+
private readonly now;
|
|
13
|
+
/** `<identity>`.`identity_credential`, resolved at construction. */
|
|
14
|
+
private readonly table;
|
|
15
|
+
private readonly hasher;
|
|
16
|
+
constructor(pool: Pool, now?: () => number,
|
|
17
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
18
|
+
schemas?: FrameworkSchemaNames);
|
|
19
|
+
provisionLocked(input: CredentialProvisionInput, query?: CredentialQuery): Promise<void>;
|
|
20
|
+
provisionPassword(input: PasswordCredentialProvisionInput, query?: CredentialQuery): Promise<void>;
|
|
21
|
+
stateOf(principalId: string, query?: CredentialQuery): Promise<CredentialState>;
|
|
22
|
+
private validate;
|
|
23
|
+
private run;
|
|
24
|
+
private insert;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=mariadb-credential-provisioner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mariadb-credential-provisioner.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-credential-provisioner.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,gCAAgC,EACtC,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAW7C,qBAAa,4BAA6B,YAAW,qBAAqB;IAMtE,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IANtB,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;gBAGpC,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAC7C,yFAAyF;IACzF,OAAO,CAAC,EAAE,oBAAoB;IAQ1B,eAAe,CAAC,KAAK,EAAE,wBAAwB,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxF,iBAAiB,CACrB,KAAK,EAAE,gCAAgC,EACvC,KAAK,CAAC,EAAE,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC;IAQV,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IAUrF,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,GAAG;YAIG,MAAM;CA0BrB"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MariaDbCredentialProvisioner = void 0;
|
|
4
|
+
const credential_provisioner_js_1 = require("./credential-provisioner.js");
|
|
5
|
+
const argon2id_password_hasher_js_1 = require("./argon2id-password-hasher.js");
|
|
6
|
+
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
7
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
8
|
+
const ER_DUP_ENTRY = 1062;
|
|
9
|
+
function requireText(value, name) {
|
|
10
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
11
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class MariaDbCredentialProvisioner {
|
|
15
|
+
pool;
|
|
16
|
+
now;
|
|
17
|
+
/** `<identity>`.`identity_credential`, resolved at construction. */
|
|
18
|
+
table;
|
|
19
|
+
hasher = new argon2id_password_hasher_js_1.Argon2idPasswordHasher();
|
|
20
|
+
constructor(pool, now = Date.now,
|
|
21
|
+
/** Framework database names; absent ⇒ resolved from `SHIELD_DB_NAMESPACE` here, once. */
|
|
22
|
+
schemas) {
|
|
23
|
+
this.pool = pool;
|
|
24
|
+
this.now = now;
|
|
25
|
+
this.table = (0, framework_schema_names_js_1.qualifiedTableName)((0, framework_schema_names_js_1.schemaNamesAtConstruction)(schemas).identity, 'identity_credential');
|
|
26
|
+
}
|
|
27
|
+
async provisionLocked(input, query) {
|
|
28
|
+
this.validate(input);
|
|
29
|
+
await this.insert(input, await (0, argon2id_password_hasher_js_1.lockedPasswordHash)(), query);
|
|
30
|
+
}
|
|
31
|
+
async provisionPassword(input, query) {
|
|
32
|
+
this.validate(input);
|
|
33
|
+
if (typeof input.password !== 'string' || input.password.length < MIN_PASSWORD_LENGTH) {
|
|
34
|
+
throw new Error(`password must be at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
35
|
+
}
|
|
36
|
+
await this.insert(input, await this.hasher.hash(input.password), query);
|
|
37
|
+
}
|
|
38
|
+
async stateOf(principalId, query) {
|
|
39
|
+
const rows = (await this.run(query)(`SELECT ic_password_hash FROM ${this.table} WHERE ic_principal_id = ? LIMIT 1`, [principalId]));
|
|
40
|
+
const row = rows[0];
|
|
41
|
+
if (!row)
|
|
42
|
+
return 'none';
|
|
43
|
+
return (0, argon2id_password_hasher_js_1.isLockedHash)(row.ic_password_hash) ? 'locked' : 'usable';
|
|
44
|
+
}
|
|
45
|
+
validate(input) {
|
|
46
|
+
requireText(input.principalId, 'principalId');
|
|
47
|
+
requireText(input.hint, 'hint');
|
|
48
|
+
requireText(input.tenantId, 'tenantId');
|
|
49
|
+
}
|
|
50
|
+
run(query) {
|
|
51
|
+
return query ?? ((sql, params) => this.pool.query(sql, params));
|
|
52
|
+
}
|
|
53
|
+
async insert(input, hash, query) {
|
|
54
|
+
const run = this.run(query);
|
|
55
|
+
const existing = (await run(`SELECT ic_identity_hint FROM ${this.table} WHERE ic_principal_id = ? LIMIT 1 FOR UPDATE`, [input.principalId]));
|
|
56
|
+
if (existing.length > 0)
|
|
57
|
+
throw new credential_provisioner_js_1.CredentialExistsError();
|
|
58
|
+
const nowMs = this.now();
|
|
59
|
+
try {
|
|
60
|
+
await run(`INSERT INTO ${this.table}
|
|
61
|
+
(ic_identity_hint, ic_password_hash, ic_principal_id, ic_tenant_id, ic_roles, ic_created_at_ms, ic_updated_at_ms)
|
|
62
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [input.hint, hash, input.principalId, input.tenantId, '[]', nowMs, nowMs]);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (err?.errno === ER_DUP_ENTRY) {
|
|
66
|
+
throw new credential_provisioner_js_1.CredentialExistsError();
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.MariaDbCredentialProvisioner = MariaDbCredentialProvisioner;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `withOperationMarkerCheck` — the guard-side seal on core gate 7's delegation marker (G-009 R1-2).
|
|
3
|
+
*
|
|
4
|
+
* Core's `decide()` reads `context.attributes['shield.delegation']` and delegates whatever the
|
|
5
|
+
* principal is. The marker therefore must come only from a principal the engine bound to that
|
|
6
|
+
* exact operation step (`operationOf`, an unforgeable WeakMap binding; the engine's
|
|
7
|
+
* `withDelegation` sets the matching marker). Here, every decision entry the authority exposes
|
|
8
|
+
* (`decide`) passes the attribute through ONLY when `operationOf(principal)` exists and its
|
|
9
|
+
* `operationKey` and `stepKey` equal the marker's; in every other case it is removed before the
|
|
10
|
+
* inner `decide()`, so the baseline decision stands (Shield #1, #9).
|
|
11
|
+
*/
|
|
12
|
+
import type { DecisionAuthority } from '@astram/core';
|
|
13
|
+
export declare function withOperationMarkerCheck(inner: DecisionAuthority): DecisionAuthority;
|
|
14
|
+
//# sourceMappingURL=operation-marker-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-marker-check.d.ts","sourceRoot":"","sources":["../../src/authz/operation-marker-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAKtD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,CAoBpF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withOperationMarkerCheck = withOperationMarkerCheck;
|
|
4
|
+
const engine_1 = require("@astram/engine");
|
|
5
|
+
const MARKER = 'shield.delegation';
|
|
6
|
+
function withOperationMarkerCheck(inner) {
|
|
7
|
+
return {
|
|
8
|
+
decide: (principal, action, resource, context) => {
|
|
9
|
+
const attributes = context.attributes;
|
|
10
|
+
if (attributes === undefined || !(MARKER in attributes)) {
|
|
11
|
+
return inner.decide(principal, action, resource, context);
|
|
12
|
+
}
|
|
13
|
+
const raw = attributes[MARKER];
|
|
14
|
+
const binding = (0, engine_1.operationOf)(principal);
|
|
15
|
+
const { operationKey, stepKey } = typeof raw === 'object' && raw !== null ? raw : {};
|
|
16
|
+
const genuine = binding !== undefined &&
|
|
17
|
+
binding.operationKey === operationKey &&
|
|
18
|
+
binding.stepKey === stepKey;
|
|
19
|
+
if (genuine)
|
|
20
|
+
return inner.decide(principal, action, resource, context);
|
|
21
|
+
const { [MARKER]: _forged, ...rest } = attributes;
|
|
22
|
+
return inner.decide(principal, action, resource, { ...context, attributes: rest });
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire-record-authorization.d.ts","sourceRoot":"","sources":["../../src/authz/wire-record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EAEd,mBAAmB,EACnB,SAAS,EAET,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"wire-record-authorization.d.ts","sourceRoot":"","sources":["../../src/authz/wire-record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EAEd,mBAAmB,EACnB,SAAS,EAET,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,kCAAkC,CAAC;AAO1C,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAEL,KAAK,sBAAsB,EAC5B,MAAM,2CAA2C,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,uFAAuF;IACvF,QAAQ,CAAC,eAAe,EAAE,mBAAmB,CAAC;IAC9C,6FAA6F;IAC7F,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAC1C,6GAA6G;IAC7G,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;CACzC;AAOD;;;;;GAKG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE,uBAAuB,GAC7B,mBAAmB,CA2BrB;AAED,6EAA6E;AAC7E,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACjD;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,yBAAyB,EACjC,OAAO,GAAE,+BAAoC,GAC5C,iBAAiB,CA8BnB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CAAC;AAEjD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,yBAAyB,GAAG,WAAW,CAyE/E"}
|
|
@@ -8,6 +8,7 @@ const mariadb_record_rule_adapter_js_1 = require("./mariadb-record-rule-adapter.
|
|
|
8
8
|
const mariadb_record_share_adapter_js_1 = require("./mariadb-record-share-adapter.js");
|
|
9
9
|
const mariadb_cross_tenant_grant_adapter_js_1 = require("./mariadb-cross-tenant-grant-adapter.js");
|
|
10
10
|
const mariadb_list_scope_adapter_js_1 = require("./mariadb-list-scope-adapter.js");
|
|
11
|
+
const operation_marker_check_js_1 = require("./operation-marker-check.js");
|
|
11
12
|
const list_scope_sql_js_1 = require("./list-scope-sql.js");
|
|
12
13
|
const session_facts_als_js_1 = require("../request-auth/session-facts-als.js");
|
|
13
14
|
const framework_schema_names_js_1 = require("../config/framework-schema-names.js");
|
|
@@ -71,7 +72,7 @@ function buildRecordAuthorization(inputs, options = {}) {
|
|
|
71
72
|
? withRedisPermissionVersion(inputs.permissionFacts, options.versionCache)
|
|
72
73
|
: inputs.permissionFacts;
|
|
73
74
|
const schema = authzSchemaOf(inputs);
|
|
74
|
-
return (0, core_1.createWiredDecisionAuthority)({
|
|
75
|
+
return (0, operation_marker_check_js_1.withOperationMarkerCheck)((0, core_1.createWiredDecisionAuthority)({
|
|
75
76
|
registry: inputs.registry,
|
|
76
77
|
// The app-owned port, wrapped so a gate-5 reload inside an enforcement request notifies the
|
|
77
78
|
// guard (security hardening Phase 1C: a permission-version move re-derives the session's
|
|
@@ -92,7 +93,7 @@ function buildRecordAuthorization(inputs, options = {}) {
|
|
|
92
93
|
// Delegation / trusted-operation authority over the DECLARED manifest (no DB).
|
|
93
94
|
// Empty ⇒ pass-through. The app supplies the entries; the framework owns the adapter.
|
|
94
95
|
operationAuthority: new manifest_operation_authority_adapter_js_1.ManifestOperationAuthorityAdapter(inputs.operationManifest ?? []),
|
|
95
|
-
});
|
|
96
|
+
}));
|
|
96
97
|
}
|
|
97
98
|
/**
|
|
98
99
|
* Construct the engine-facing {@link ListScopeFn} — the LIST-form sibling to
|
|
@@ -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"}
|