@astram/guard 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/auth.module.d.ts +6 -0
- package/dist/auth/auth.module.d.ts.map +1 -1
- package/dist/auth/auth.module.js +106 -15
- package/dist/auth/tokens.d.ts +9 -0
- package/dist/auth/tokens.d.ts.map +1 -1
- package/dist/auth/tokens.js +10 -1
- package/dist/authentication/log-email-sender.d.ts +13 -0
- package/dist/authentication/log-email-sender.d.ts.map +1 -0
- package/dist/authentication/log-email-sender.js +13 -0
- package/dist/authentication/mariadb-credential-writer.d.ts +16 -0
- package/dist/authentication/mariadb-credential-writer.d.ts.map +1 -0
- package/dist/authentication/mariadb-credential-writer.js +19 -0
- package/dist/authentication/mariadb-mfa-device-store.d.ts +16 -0
- package/dist/authentication/mariadb-mfa-device-store.d.ts.map +1 -0
- package/dist/authentication/mariadb-mfa-device-store.js +99 -0
- package/dist/authentication/mariadb-or-totp-verifier.d.ts +31 -0
- package/dist/authentication/mariadb-or-totp-verifier.d.ts.map +1 -0
- package/dist/authentication/mariadb-or-totp-verifier.js +48 -0
- package/dist/authentication/mariadb-profile-resolver.d.ts +24 -0
- package/dist/authentication/mariadb-profile-resolver.d.ts.map +1 -0
- package/dist/authentication/mariadb-profile-resolver.js +54 -0
- package/dist/authentication/mariadb-profile-writer.d.ts +18 -0
- package/dist/authentication/mariadb-profile-writer.d.ts.map +1 -0
- package/dist/authentication/mariadb-profile-writer.js +36 -0
- package/dist/authentication/mariadb-recovery-code-store.d.ts +47 -0
- package/dist/authentication/mariadb-recovery-code-store.d.ts.map +1 -0
- package/dist/authentication/mariadb-recovery-code-store.js +53 -0
- package/dist/authentication/mariadb-reset-token-store.d.ts +26 -0
- package/dist/authentication/mariadb-reset-token-store.d.ts.map +1 -0
- package/dist/authentication/mariadb-reset-token-store.js +43 -0
- package/dist/authentication/mariadb-session-sweeper.d.ts +33 -0
- package/dist/authentication/mariadb-session-sweeper.d.ts.map +1 -0
- package/dist/authentication/mariadb-session-sweeper.js +60 -0
- package/dist/identity/auth-facts.controller.d.ts +24 -0
- package/dist/identity/auth-facts.controller.d.ts.map +1 -0
- package/dist/identity/auth-facts.controller.js +106 -0
- package/dist/identity/identity-actions.d.ts +22 -0
- package/dist/identity/identity-actions.d.ts.map +1 -0
- package/dist/identity/identity-actions.js +32 -0
- package/dist/identity/module-registry.bridge.d.ts +4 -0
- package/dist/identity/module-registry.bridge.d.ts.map +1 -0
- package/dist/identity/module-registry.bridge.js +43 -0
- package/dist/identity/permission-facts.adapter.d.ts +47 -0
- package/dist/identity/permission-facts.adapter.d.ts.map +1 -0
- package/dist/identity/permission-facts.adapter.js +108 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -2
- package/dist/migrations-dir.d.ts +2 -0
- package/dist/migrations-dir.d.ts.map +1 -0
- package/dist/migrations-dir.js +13 -0
- package/dist/module/shield-tokens.d.ts +6 -1
- package/dist/module/shield-tokens.d.ts.map +1 -1
- package/dist/module/shield-tokens.js +7 -1
- package/dist/module/shield.module.d.ts +17 -0
- package/dist/module/shield.module.d.ts.map +1 -1
- package/dist/module/shield.module.js +12 -0
- package/migrations/1710000000006-CreateAuthzSchema.cjs +53 -0
- package/migrations/1710000000007-ExtendPermissionAndRolePermission.cjs +66 -0
- package/migrations/1710000000008-CreateAccessBundleTables.cjs +49 -0
- package/package.json +5 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MariaDbProfileWriter — the framework-DEFAULT ProfileWriter (backs PATCH /account/profile). Updates
|
|
3
|
+
* ONLY the supplied columns on shield_app.users, then re-reads via MariaDbProfileResolver so the returned
|
|
4
|
+
* display profile has ONE source of truth (names, never UUIDs). Fail-closed: throws if the re-read is null.
|
|
5
|
+
*/
|
|
6
|
+
import type { Pool } from 'mariadb';
|
|
7
|
+
import type { ProfileWriter } from '../auth/ports/profile-writer.js';
|
|
8
|
+
import type { CurrentUserProfile } from '../auth/profile-resolver.js';
|
|
9
|
+
export declare class MariaDbProfileWriter implements ProfileWriter {
|
|
10
|
+
private readonly pool;
|
|
11
|
+
private readonly resolver;
|
|
12
|
+
constructor(pool: Pool);
|
|
13
|
+
update(principalId: string, changes: {
|
|
14
|
+
name?: string;
|
|
15
|
+
email?: string;
|
|
16
|
+
}): Promise<CurrentUserProfile>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=mariadb-profile-writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mariadb-profile-writer.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-profile-writer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAOtE,qBAAa,oBAAqB,YAAW,aAAa;IAE5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IADjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;gBACrB,IAAI,EAAE,IAAI;IAIjC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA0B3G"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MariaDbProfileWriter = void 0;
|
|
4
|
+
const mariadb_profile_resolver_js_1 = require("./mariadb-profile-resolver.js");
|
|
5
|
+
class MariaDbProfileWriter {
|
|
6
|
+
pool;
|
|
7
|
+
resolver;
|
|
8
|
+
constructor(pool) {
|
|
9
|
+
this.pool = pool;
|
|
10
|
+
this.resolver = new mariadb_profile_resolver_js_1.MariaDbProfileResolver(pool);
|
|
11
|
+
}
|
|
12
|
+
async update(principalId, changes) {
|
|
13
|
+
const sets = [];
|
|
14
|
+
const params = [];
|
|
15
|
+
if (changes.name !== undefined) {
|
|
16
|
+
sets.push('u_username = ?');
|
|
17
|
+
params.push(changes.name);
|
|
18
|
+
}
|
|
19
|
+
if (changes.email !== undefined) {
|
|
20
|
+
sets.push('u_email = ?');
|
|
21
|
+
params.push(changes.email);
|
|
22
|
+
}
|
|
23
|
+
if (sets.length > 0) {
|
|
24
|
+
params.push(principalId);
|
|
25
|
+
await this.pool.query(`UPDATE \`shield_app\`.\`users\` SET ${sets.join(', ')} WHERE u_uuid = ?`, params);
|
|
26
|
+
}
|
|
27
|
+
// Resolve the caller's active company so the re-read profile is tenant-correct.
|
|
28
|
+
const active = (await this.pool.query("SELECT cm_company_uuid FROM `shield_app`.`company_membership` WHERE cm_user_uuid = ? AND cm_is_active = 1 AND cm_status = 'active' LIMIT 1", [principalId]));
|
|
29
|
+
const tenantId = active[0]?.cm_company_uuid ?? '';
|
|
30
|
+
const profile = await this.resolver.resolve({ kind: 'user', id: principalId, tenantId, roleIds: [], isRoot: false });
|
|
31
|
+
if (profile === null)
|
|
32
|
+
throw new Error('profile_unavailable'); // fail-closed: never a partial write result
|
|
33
|
+
return profile;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.MariaDbProfileWriter = MariaDbProfileWriter;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MariaDbRecoveryCodeStore — the framework-DEFAULT RecoveryCodeStore (backs
|
|
3
|
+
* POST /account/mfa/recovery-codes/regenerate + recovery-code login), persisted in
|
|
4
|
+
* `shield_identity`.`mfa_recovery_code` (one row per Argon2-hashed one-time recovery
|
|
5
|
+
* code, PK `(mrc_principal_id, mrc_code_hash)`).
|
|
6
|
+
*
|
|
7
|
+
* The framework hands this store ALREADY-HASHED codes (Argon2), so `replaceAll` never
|
|
8
|
+
* sees the raw codes — it only wipes-then-inserts hashes. `consume` receives the RAW
|
|
9
|
+
* code the user presents; because Argon2 hashes are salted and NOT queryable by
|
|
10
|
+
* plaintext, it loads the principal's UNCONSUMED rows and verifies the raw code against
|
|
11
|
+
* each hash with the injected hasher until one matches, then atomically marks THAT row
|
|
12
|
+
* consumed (single-use). A conditional `UPDATE … WHERE mrc_consumed_at_ms IS NULL` with
|
|
13
|
+
* an `affectedRows === 1` race guard makes the mark single-winner: two concurrent
|
|
14
|
+
* consumes of the same code both verify, but only one wins the UPDATE; the loser treats
|
|
15
|
+
* it as already-consumed → `false`. Every query is parameterized (never string-built).
|
|
16
|
+
*
|
|
17
|
+
* SECURITY — never logs the raw code or a hash; a hash mismatch resolves to "not this
|
|
18
|
+
* code" (fail-closed), never a thrown error. A genuine store outage (the `query` call
|
|
19
|
+
* rejecting) propagates rather than being swallowed to `false`.
|
|
20
|
+
*/
|
|
21
|
+
import type { Pool } from 'mariadb';
|
|
22
|
+
import type { PasswordHasher } from '@astram/core';
|
|
23
|
+
import type { RecoveryCodeStore } from '../auth/ports/recovery-code-store.js';
|
|
24
|
+
export declare class MariaDbRecoveryCodeStore implements RecoveryCodeStore {
|
|
25
|
+
private readonly pool;
|
|
26
|
+
/** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
|
|
27
|
+
private readonly hasher;
|
|
28
|
+
private readonly now;
|
|
29
|
+
constructor(pool: Pool,
|
|
30
|
+
/** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
|
|
31
|
+
hasher: PasswordHasher, now?: () => number);
|
|
32
|
+
/**
|
|
33
|
+
* Replace the principal's entire recovery-code set with `codeHashes` (already
|
|
34
|
+
* Argon2-hashed). Wipes any prior codes first, then inserts the new batch. Passing
|
|
35
|
+
* an empty array simply wipes (used when MFA is disabled).
|
|
36
|
+
*/
|
|
37
|
+
replaceAll(principalId: string, codeHashes: readonly string[]): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Consume one of the principal's recovery codes by RAW value. Loads the unconsumed
|
|
40
|
+
* hashes, verifies the raw code against each, and on the first match atomically marks
|
|
41
|
+
* that row consumed (single-use, `affectedRows === 1` race guard). Returns `true` iff
|
|
42
|
+
* a code was matched AND won the consume; `false` for empty input, no match, an
|
|
43
|
+
* already-consumed code, or a lost race.
|
|
44
|
+
*/
|
|
45
|
+
consume(principalId: string, rawCode: string): Promise<boolean>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=mariadb-recovery-code-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mariadb-recovery-code-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-recovery-code-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAM9E,qBAAa,wBAAyB,YAAW,iBAAiB;IAE9D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAHH,IAAI,EAAE,IAAI;IAC3B,+EAA+E;IAC9D,MAAM,EAAE,cAAc,EACtB,GAAG,GAAE,MAAM,MAAiB;IAG/C;;;;OAIG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBnF;;;;;;OAMG;IACG,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAqBtE"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MariaDbRecoveryCodeStore = void 0;
|
|
4
|
+
class MariaDbRecoveryCodeStore {
|
|
5
|
+
pool;
|
|
6
|
+
hasher;
|
|
7
|
+
now;
|
|
8
|
+
constructor(pool,
|
|
9
|
+
/** The SAME Argon2id hasher the framework uses to hash the codes on enroll. */
|
|
10
|
+
hasher, now = Date.now) {
|
|
11
|
+
this.pool = pool;
|
|
12
|
+
this.hasher = hasher;
|
|
13
|
+
this.now = now;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Replace the principal's entire recovery-code set with `codeHashes` (already
|
|
17
|
+
* Argon2-hashed). Wipes any prior codes first, then inserts the new batch. Passing
|
|
18
|
+
* an empty array simply wipes (used when MFA is disabled).
|
|
19
|
+
*/
|
|
20
|
+
async replaceAll(principalId, codeHashes) {
|
|
21
|
+
await this.pool.query(`DELETE FROM \`shield_identity\`.\`mfa_recovery_code\` WHERE mrc_principal_id = ?`, [principalId]);
|
|
22
|
+
const createdAt = this.now();
|
|
23
|
+
for (const hash of codeHashes) {
|
|
24
|
+
await this.pool.query(`INSERT INTO \`shield_identity\`.\`mfa_recovery_code\`
|
|
25
|
+
(mrc_principal_id, mrc_code_hash, mrc_consumed_at_ms, mrc_created_at_ms)
|
|
26
|
+
VALUES (?, ?, NULL, ?)`, [principalId, hash, createdAt]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Consume one of the principal's recovery codes by RAW value. Loads the unconsumed
|
|
31
|
+
* hashes, verifies the raw code against each, and on the first match atomically marks
|
|
32
|
+
* that row consumed (single-use, `affectedRows === 1` race guard). Returns `true` iff
|
|
33
|
+
* a code was matched AND won the consume; `false` for empty input, no match, an
|
|
34
|
+
* already-consumed code, or a lost race.
|
|
35
|
+
*/
|
|
36
|
+
async consume(principalId, rawCode) {
|
|
37
|
+
if (typeof rawCode !== 'string' || rawCode.length === 0)
|
|
38
|
+
return false;
|
|
39
|
+
const rows = (await this.pool.query(`SELECT mrc_code_hash FROM \`shield_identity\`.\`mfa_recovery_code\`
|
|
40
|
+
WHERE mrc_principal_id = ? AND mrc_consumed_at_ms IS NULL`, [principalId]));
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
if (await this.hasher.verify(row.mrc_code_hash, rawCode)) {
|
|
43
|
+
const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`mfa_recovery_code\`
|
|
44
|
+
SET mrc_consumed_at_ms = ?
|
|
45
|
+
WHERE mrc_principal_id = ? AND mrc_code_hash = ? AND mrc_consumed_at_ms IS NULL`, [this.now(), principalId, row.mrc_code_hash]));
|
|
46
|
+
// Single-use guard: only the UPDATE that flips NULL→consumed wins.
|
|
47
|
+
return res.affectedRows === 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.MariaDbRecoveryCodeStore = MariaDbRecoveryCodeStore;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MariaDbResetTokenStore — the framework-DEFAULT ResetTokenStore (backs POST /auth/forgot-password
|
|
3
|
+
* + /auth/reset-password), persisted in `shield_identity`.`password_reset_token`. The framework
|
|
4
|
+
* hashes the raw reset token (SHA-256) BEFORE it reaches this store, so the store never sees the
|
|
5
|
+
* raw token — only its hash (the primary key).
|
|
6
|
+
*
|
|
7
|
+
* SINGLE-USE + TTL: `consume` returns the principal id IFF the token is both unexpired AND
|
|
8
|
+
* unconsumed, then atomically marks it consumed. It is a SELECT-then-conditional-UPDATE with an
|
|
9
|
+
* `affectedRows` race guard: two concurrent consumes of the same token both pass the SELECT, but
|
|
10
|
+
* only ONE wins the `UPDATE … WHERE prt_consumed_at_ms IS NULL` (affectedRows === 1); the loser
|
|
11
|
+
* (affectedRows === 0) is treated as already-consumed → `null`. Unknown/expired/already-consumed
|
|
12
|
+
* tokens all return `null` too — never a distinguishing throw (anti-enumeration). A genuine store
|
|
13
|
+
* outage (the `query` call rejecting) propagates rather than being swallowed to `null`. Every
|
|
14
|
+
* query is parameterized (never string-built); the token hash is never logged.
|
|
15
|
+
*/
|
|
16
|
+
import type { Pool } from 'mariadb';
|
|
17
|
+
import type { ResetTokenStore } from '../auth/ports/reset-token-store.js';
|
|
18
|
+
export declare class MariaDbResetTokenStore implements ResetTokenStore {
|
|
19
|
+
private readonly pool;
|
|
20
|
+
private readonly now;
|
|
21
|
+
constructor(pool: Pool, now?: () => number);
|
|
22
|
+
issue(principalId: string, tokenHash: string, expiresAtMs: number): Promise<void>;
|
|
23
|
+
consume(tokenHash: string): Promise<string | null>;
|
|
24
|
+
invalidateAllForPrincipal(principalId: string): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=mariadb-reset-token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mariadb-reset-token-store.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-reset-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAM1E,qBAAa,sBAAuB,YAAW,eAAe;IAE1D,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBADH,IAAI,EAAE,IAAI,EACV,GAAG,GAAE,MAAM,MAAiB;IAGzC,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASjF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0BlD,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAUpE"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MariaDbResetTokenStore = void 0;
|
|
4
|
+
class MariaDbResetTokenStore {
|
|
5
|
+
pool;
|
|
6
|
+
now;
|
|
7
|
+
constructor(pool, now = Date.now) {
|
|
8
|
+
this.pool = pool;
|
|
9
|
+
this.now = now;
|
|
10
|
+
}
|
|
11
|
+
async issue(principalId, tokenHash, expiresAtMs) {
|
|
12
|
+
await this.pool.query(`INSERT INTO \`shield_identity\`.\`password_reset_token\`
|
|
13
|
+
(prt_token_hash, prt_principal_id, prt_expires_at_ms, prt_consumed_at_ms, prt_created_at_ms)
|
|
14
|
+
VALUES (?, ?, ?, NULL, ?)`, [tokenHash, principalId, expiresAtMs, this.now()]);
|
|
15
|
+
}
|
|
16
|
+
async consume(tokenHash) {
|
|
17
|
+
const now = this.now();
|
|
18
|
+
// 1) Find an unexpired, unconsumed token for this hash.
|
|
19
|
+
const rows = (await this.pool.query(`SELECT prt_principal_id
|
|
20
|
+
FROM \`shield_identity\`.\`password_reset_token\`
|
|
21
|
+
WHERE prt_token_hash = ? AND prt_consumed_at_ms IS NULL AND prt_expires_at_ms > ?
|
|
22
|
+
LIMIT 1`, [tokenHash, now]));
|
|
23
|
+
const principalId = rows[0]?.prt_principal_id;
|
|
24
|
+
if (principalId === undefined)
|
|
25
|
+
return null;
|
|
26
|
+
// 2) Atomically claim it (single-use). The `IS NULL` guard + affectedRows check means a
|
|
27
|
+
// concurrent consume can never both succeed — the loser sees 0 rows and returns null.
|
|
28
|
+
const res = (await this.pool.query(`UPDATE \`shield_identity\`.\`password_reset_token\`
|
|
29
|
+
SET prt_consumed_at_ms = ?
|
|
30
|
+
WHERE prt_token_hash = ? AND prt_consumed_at_ms IS NULL`, [now, tokenHash]));
|
|
31
|
+
if (res.affectedRows !== 1)
|
|
32
|
+
return null; // lost the race → treat as consumed
|
|
33
|
+
return principalId;
|
|
34
|
+
}
|
|
35
|
+
async invalidateAllForPrincipal(principalId) {
|
|
36
|
+
// Burn every outstanding (unconsumed) token for the principal after a successful reset, so a
|
|
37
|
+
// second still-valid link cannot re-reset.
|
|
38
|
+
await this.pool.query(`UPDATE \`shield_identity\`.\`password_reset_token\`
|
|
39
|
+
SET prt_consumed_at_ms = ?
|
|
40
|
+
WHERE prt_principal_id = ? AND prt_consumed_at_ms IS NULL`, [this.now(), principalId]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.MariaDbResetTokenStore = MariaDbResetTokenStore;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MariaDbSessionSweeper — the framework-DEFAULT SessionSweeper (backs the best-effort
|
|
3
|
+
* calls from POST /account/change-password + /auth/reset-password), revoking a
|
|
4
|
+
* principal's durable sessions in `shield_identity`.`session` after a credential
|
|
5
|
+
* change.
|
|
6
|
+
*
|
|
7
|
+
* - revokeAllForPrincipal → after a password RESET (kill every session).
|
|
8
|
+
* - revokeOthersForPrincipal → after an authenticated CHANGE-password (kill every
|
|
9
|
+
* session except the caller's current one, so the actor stays signed in).
|
|
10
|
+
*
|
|
11
|
+
* WHY WE ALSO DROP THE REDIS LIVENESS KEY: flipping the durable `session` row to
|
|
12
|
+
* 'revoked' makes the session non-rotatable (refresh is rejected), but an
|
|
13
|
+
* outstanding ACCESS token's per-request liveness is gated on the Redis key the
|
|
14
|
+
* `SessionTokenBinder` owns (see its SESSION-LIVENESS GATE in `resolve()`). The
|
|
15
|
+
* durable row alone does not reach that key, so a still-valid access token would
|
|
16
|
+
* survive until its TTL. To make revocation IMMEDIATE, we also drop the liveness
|
|
17
|
+
* key for each revoked session via `binder.unbindSession` — the SAME mechanism
|
|
18
|
+
* logout uses.
|
|
19
|
+
*
|
|
20
|
+
* The binder is passed in at construction time rather than DI-resolved here (this
|
|
21
|
+
* class is a plain adapter; wiring lives with the consumer).
|
|
22
|
+
*/
|
|
23
|
+
import type { Pool } from 'mariadb';
|
|
24
|
+
import type { SessionSweeper } from '../auth/ports/session-sweeper.js';
|
|
25
|
+
import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
|
|
26
|
+
export declare class MariaDbSessionSweeper implements SessionSweeper {
|
|
27
|
+
private readonly pool;
|
|
28
|
+
private readonly binder;
|
|
29
|
+
constructor(pool: Pool, binder: SessionTokenBinder);
|
|
30
|
+
revokeAllForPrincipal(principalId: string): Promise<void>;
|
|
31
|
+
revokeOthersForPrincipal(principalId: string, keepSessionId: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=mariadb-session-sweeper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mariadb-session-sweeper.d.ts","sourceRoot":"","sources":["../../src/authentication/mariadb-session-sweeper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAMlF,qBAAa,qBAAsB,YAAW,cAAc;IAExD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,kBAAkB;IAGvC,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBzD,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAmB1F"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MariaDbSessionSweeper — the framework-DEFAULT SessionSweeper (backs the best-effort
|
|
4
|
+
* calls from POST /account/change-password + /auth/reset-password), revoking a
|
|
5
|
+
* principal's durable sessions in `shield_identity`.`session` after a credential
|
|
6
|
+
* change.
|
|
7
|
+
*
|
|
8
|
+
* - revokeAllForPrincipal → after a password RESET (kill every session).
|
|
9
|
+
* - revokeOthersForPrincipal → after an authenticated CHANGE-password (kill every
|
|
10
|
+
* session except the caller's current one, so the actor stays signed in).
|
|
11
|
+
*
|
|
12
|
+
* WHY WE ALSO DROP THE REDIS LIVENESS KEY: flipping the durable `session` row to
|
|
13
|
+
* 'revoked' makes the session non-rotatable (refresh is rejected), but an
|
|
14
|
+
* outstanding ACCESS token's per-request liveness is gated on the Redis key the
|
|
15
|
+
* `SessionTokenBinder` owns (see its SESSION-LIVENESS GATE in `resolve()`). The
|
|
16
|
+
* durable row alone does not reach that key, so a still-valid access token would
|
|
17
|
+
* survive until its TTL. To make revocation IMMEDIATE, we also drop the liveness
|
|
18
|
+
* key for each revoked session via `binder.unbindSession` — the SAME mechanism
|
|
19
|
+
* logout uses.
|
|
20
|
+
*
|
|
21
|
+
* The binder is passed in at construction time rather than DI-resolved here (this
|
|
22
|
+
* class is a plain adapter; wiring lives with the consumer).
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.MariaDbSessionSweeper = void 0;
|
|
26
|
+
class MariaDbSessionSweeper {
|
|
27
|
+
pool;
|
|
28
|
+
binder;
|
|
29
|
+
constructor(pool, binder) {
|
|
30
|
+
this.pool = pool;
|
|
31
|
+
this.binder = binder;
|
|
32
|
+
}
|
|
33
|
+
async revokeAllForPrincipal(principalId) {
|
|
34
|
+
const rows = (await this.pool.query(`SELECT s_session_id
|
|
35
|
+
FROM \`shield_identity\`.\`session\`
|
|
36
|
+
WHERE s_principal_id = ? AND s_status = 'active'`, [principalId]));
|
|
37
|
+
await this.pool.query(`UPDATE \`shield_identity\`.\`session\`
|
|
38
|
+
SET s_status = 'revoked'
|
|
39
|
+
WHERE s_principal_id = ? AND s_status = 'active'`, [principalId]);
|
|
40
|
+
// Drop each revoked session's Redis liveness key so outstanding access tokens die
|
|
41
|
+
// immediately, not at their TTL.
|
|
42
|
+
for (const { s_session_id } of rows) {
|
|
43
|
+
await this.binder.unbindSession(s_session_id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async revokeOthersForPrincipal(principalId, keepSessionId) {
|
|
47
|
+
const rows = (await this.pool.query(`SELECT s_session_id
|
|
48
|
+
FROM \`shield_identity\`.\`session\`
|
|
49
|
+
WHERE s_principal_id = ? AND s_status = 'active' AND s_session_id <> ?`, [principalId, keepSessionId]));
|
|
50
|
+
await this.pool.query(`UPDATE \`shield_identity\`.\`session\`
|
|
51
|
+
SET s_status = 'revoked'
|
|
52
|
+
WHERE s_principal_id = ? AND s_status = 'active' AND s_session_id <> ?`, [principalId, keepSessionId]);
|
|
53
|
+
// Only the OTHER sessions are unbound — the kept (current) session's liveness key
|
|
54
|
+
// is never touched, so the actor stays signed in after a change-password.
|
|
55
|
+
for (const { s_session_id } of rows) {
|
|
56
|
+
await this.binder.unbindSession(s_session_id);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.MariaDbSessionSweeper = MariaDbSessionSweeper;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthFactsController — the framework-DEFAULT `/identity/auth-facts` + `/identity/permissions-version`
|
|
3
|
+
* endpoints backing the frontend's permission-gated nav (the SDK's fact projection). Both are
|
|
4
|
+
* `@Public()` at the route level but SELF-SCOPED: principalId + tenantId come ONLY from the resolved
|
|
5
|
+
* bearer-token Principal (never a query/body param). Missing/invalid token → 401; an authorization
|
|
6
|
+
* store outage → 503 (never swallowed into an empty projection / fabricated version — that would
|
|
7
|
+
* fail-OPEN by letting a stale/empty view read as authoritative).
|
|
8
|
+
*
|
|
9
|
+
* FRAMEWORK-OWNED: mounted by the auth surface when `auth: true`; the consumer implements nothing.
|
|
10
|
+
*/
|
|
11
|
+
import type { SessionTokenBinder } from '../request-auth/session-token-binder.js';
|
|
12
|
+
import { type AuthFactProjection, type MariaDbPermissionFactsAdapter } from './permission-facts.adapter.js';
|
|
13
|
+
export declare class AuthFactsController {
|
|
14
|
+
private readonly tokenBinder;
|
|
15
|
+
private readonly facts;
|
|
16
|
+
constructor(tokenBinder: SessionTokenBinder | undefined, facts: MariaDbPermissionFactsAdapter | undefined);
|
|
17
|
+
getAuthFacts(authorization?: string): Promise<AuthFactProjection>;
|
|
18
|
+
getPermissionsVersion(authorization?: string): Promise<{
|
|
19
|
+
version: number;
|
|
20
|
+
}>;
|
|
21
|
+
/** Resolve the bearer token → the caller's own Principal, or throw a generic 401. */
|
|
22
|
+
private authenticate;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=auth-facts.controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-facts.controller.d.ts","sourceRoot":"","sources":["../../src/identity/auth-facts.controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAGlF,OAAO,EAAiC,KAAK,kBAAkB,EAAE,KAAK,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAE3I,qBACa,mBAAmB;IAEqB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK;gBADD,WAAW,EAAE,kBAAkB,GAAG,SAAS,EAC/C,KAAK,EAAE,6BAA6B,GAAG,SAAS;IAK1G,YAAY,CAA2B,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAiB3F,qBAAqB,CAA2B,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAgB3G,qFAAqF;YACvE,YAAY;CAU3B"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AuthFactsController — the framework-DEFAULT `/identity/auth-facts` + `/identity/permissions-version`
|
|
4
|
+
* endpoints backing the frontend's permission-gated nav (the SDK's fact projection). Both are
|
|
5
|
+
* `@Public()` at the route level but SELF-SCOPED: principalId + tenantId come ONLY from the resolved
|
|
6
|
+
* bearer-token Principal (never a query/body param). Missing/invalid token → 401; an authorization
|
|
7
|
+
* store outage → 503 (never swallowed into an empty projection / fabricated version — that would
|
|
8
|
+
* fail-OPEN by letting a stale/empty view read as authoritative).
|
|
9
|
+
*
|
|
10
|
+
* FRAMEWORK-OWNED: mounted by the auth surface when `auth: true`; the consumer implements nothing.
|
|
11
|
+
*/
|
|
12
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
13
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
14
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
15
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
16
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
17
|
+
};
|
|
18
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
19
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
20
|
+
};
|
|
21
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
22
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.AuthFactsController = void 0;
|
|
26
|
+
const common_1 = require("@nestjs/common");
|
|
27
|
+
const classification_decorator_js_1 = require("../enforcement/classification.decorator.js");
|
|
28
|
+
const tokens_js_1 = require("../request-auth/tokens.js");
|
|
29
|
+
const request_principal_js_1 = require("../request-auth/request-principal.js");
|
|
30
|
+
const tokens_js_2 = require("../auth/tokens.js");
|
|
31
|
+
const permission_facts_adapter_js_1 = require("./permission-facts.adapter.js");
|
|
32
|
+
let AuthFactsController = class AuthFactsController {
|
|
33
|
+
tokenBinder;
|
|
34
|
+
facts;
|
|
35
|
+
constructor(tokenBinder, facts) {
|
|
36
|
+
this.tokenBinder = tokenBinder;
|
|
37
|
+
this.facts = facts;
|
|
38
|
+
}
|
|
39
|
+
async getAuthFacts(authorization) {
|
|
40
|
+
const principal = await this.authenticate(authorization);
|
|
41
|
+
if (this.facts === undefined || this.facts === null) {
|
|
42
|
+
throw new common_1.HttpException({ error: 'authorization_unavailable' }, common_1.HttpStatus.SERVICE_UNAVAILABLE);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return await this.facts.projectFor(principal.id, principal.tenantId, principal.isRoot);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err instanceof permission_facts_adapter_js_1.AuthorizationUnavailableError) {
|
|
49
|
+
throw new common_1.HttpException({ error: 'authorization_unavailable' }, common_1.HttpStatus.SERVICE_UNAVAILABLE);
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async getPermissionsVersion(authorization) {
|
|
55
|
+
const principal = await this.authenticate(authorization);
|
|
56
|
+
if (this.facts === undefined || this.facts === null) {
|
|
57
|
+
throw new common_1.HttpException({ error: 'authorization_unavailable' }, common_1.HttpStatus.SERVICE_UNAVAILABLE);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const version = await this.facts.currentVersionFor(principal.id, principal.tenantId);
|
|
61
|
+
return { version };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err instanceof permission_facts_adapter_js_1.AuthorizationUnavailableError) {
|
|
65
|
+
throw new common_1.HttpException({ error: 'authorization_unavailable' }, common_1.HttpStatus.SERVICE_UNAVAILABLE);
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Resolve the bearer token → the caller's own Principal, or throw a generic 401. */
|
|
71
|
+
async authenticate(authorization) {
|
|
72
|
+
if (this.tokenBinder === undefined || this.tokenBinder === null) {
|
|
73
|
+
throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
|
|
74
|
+
}
|
|
75
|
+
const principal = await (0, request_principal_js_1.resolveRequestPrincipal)(authorization, this.tokenBinder);
|
|
76
|
+
if (principal === null) {
|
|
77
|
+
throw new common_1.HttpException({ error: 'unauthorized' }, common_1.HttpStatus.UNAUTHORIZED);
|
|
78
|
+
}
|
|
79
|
+
return principal;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
exports.AuthFactsController = AuthFactsController;
|
|
83
|
+
__decorate([
|
|
84
|
+
(0, classification_decorator_js_1.Public)(),
|
|
85
|
+
(0, common_1.Get)('auth-facts'),
|
|
86
|
+
__param(0, (0, common_1.Headers)('authorization')),
|
|
87
|
+
__metadata("design:type", Function),
|
|
88
|
+
__metadata("design:paramtypes", [String]),
|
|
89
|
+
__metadata("design:returntype", Promise)
|
|
90
|
+
], AuthFactsController.prototype, "getAuthFacts", null);
|
|
91
|
+
__decorate([
|
|
92
|
+
(0, classification_decorator_js_1.Public)(),
|
|
93
|
+
(0, common_1.Get)('permissions-version'),
|
|
94
|
+
__param(0, (0, common_1.Headers)('authorization')),
|
|
95
|
+
__metadata("design:type", Function),
|
|
96
|
+
__metadata("design:paramtypes", [String]),
|
|
97
|
+
__metadata("design:returntype", Promise)
|
|
98
|
+
], AuthFactsController.prototype, "getPermissionsVersion", null);
|
|
99
|
+
exports.AuthFactsController = AuthFactsController = __decorate([
|
|
100
|
+
(0, common_1.Controller)('identity'),
|
|
101
|
+
__param(0, (0, common_1.Optional)()),
|
|
102
|
+
__param(0, (0, common_1.Inject)(tokens_js_1.SHIELD_SESSION_TOKEN_BINDER)),
|
|
103
|
+
__param(1, (0, common_1.Optional)()),
|
|
104
|
+
__param(1, (0, common_1.Inject)(tokens_js_2.SHIELD_PERMISSION_FACTS)),
|
|
105
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
106
|
+
], AuthFactsController);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed action vocabulary + module key for the framework's @astram/identity objects.
|
|
3
|
+
* Engine ObjectTypes carry no action list, so actions are synthesized here — the ONE source
|
|
4
|
+
* the module-registry bridge derives from. Permission keys are `identity:<typeKey>:<action>`.
|
|
5
|
+
* (Ported from astram3. `cross_tenant_grant` gets its own override — see below — every other
|
|
6
|
+
* type uses the CRUD verbs.)
|
|
7
|
+
*/
|
|
8
|
+
export declare const MODULE_KEY = "identity";
|
|
9
|
+
export declare const IDENTITY_ACTIONS: readonly ["read", "create", "update", "delete"];
|
|
10
|
+
export type IdentityAction = (typeof IDENTITY_ACTIONS)[number];
|
|
11
|
+
/**
|
|
12
|
+
* `cross_tenant_grant`'s action vocabulary — NOT the CRUD verbs above. This is a
|
|
13
|
+
* framework-owned, authority-only object type (never CRUD'd through the RecordEngine);
|
|
14
|
+
* its three actions are the who-may-grant lifecycle questions the app's `authorize`
|
|
15
|
+
* closure answers for `CrossTenantGrantWriter` (offer -> 'share', accept -> 'accept',
|
|
16
|
+
* revoke -> 'revoke'). Mirrors astram3's `authz/actions.ts`'s `CROSS_TENANT_GRANT_ACTIONS`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CROSS_TENANT_GRANT_ACTIONS: readonly ["share", "accept", "revoke"];
|
|
19
|
+
export type CrossTenantGrantAction = (typeof CROSS_TENANT_GRANT_ACTIONS)[number];
|
|
20
|
+
/** The action vocabulary for `typeKey` — the per-type override if one exists, else the default. */
|
|
21
|
+
export declare function actionsFor(typeKey: string): readonly string[];
|
|
22
|
+
//# sourceMappingURL=identity-actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-actions.d.ts","sourceRoot":"","sources":["../../src/identity/identity-actions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,aAAa,CAAC;AACrC,eAAO,MAAM,gBAAgB,iDAAkD,CAAC;AAChF,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,wCAAyC,CAAC;AACjF,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAUjF,mGAAmG;AACnG,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAE7D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CROSS_TENANT_GRANT_ACTIONS = exports.IDENTITY_ACTIONS = exports.MODULE_KEY = void 0;
|
|
4
|
+
exports.actionsFor = actionsFor;
|
|
5
|
+
/**
|
|
6
|
+
* The fixed action vocabulary + module key for the framework's @astram/identity objects.
|
|
7
|
+
* Engine ObjectTypes carry no action list, so actions are synthesized here — the ONE source
|
|
8
|
+
* the module-registry bridge derives from. Permission keys are `identity:<typeKey>:<action>`.
|
|
9
|
+
* (Ported from astram3. `cross_tenant_grant` gets its own override — see below — every other
|
|
10
|
+
* type uses the CRUD verbs.)
|
|
11
|
+
*/
|
|
12
|
+
exports.MODULE_KEY = 'identity';
|
|
13
|
+
exports.IDENTITY_ACTIONS = ['read', 'create', 'update', 'delete'];
|
|
14
|
+
/**
|
|
15
|
+
* `cross_tenant_grant`'s action vocabulary — NOT the CRUD verbs above. This is a
|
|
16
|
+
* framework-owned, authority-only object type (never CRUD'd through the RecordEngine);
|
|
17
|
+
* its three actions are the who-may-grant lifecycle questions the app's `authorize`
|
|
18
|
+
* closure answers for `CrossTenantGrantWriter` (offer -> 'share', accept -> 'accept',
|
|
19
|
+
* revoke -> 'revoke'). Mirrors astram3's `authz/actions.ts`'s `CROSS_TENANT_GRANT_ACTIONS`.
|
|
20
|
+
*/
|
|
21
|
+
exports.CROSS_TENANT_GRANT_ACTIONS = ['share', 'accept', 'revoke'];
|
|
22
|
+
/**
|
|
23
|
+
* Per-type action-vocabulary override. Every type not listed here defaults to
|
|
24
|
+
* {@link IDENTITY_ACTIONS} (the CRUD verbs).
|
|
25
|
+
*/
|
|
26
|
+
const ACTIONS_BY_TYPE = {
|
|
27
|
+
cross_tenant_grant: exports.CROSS_TENANT_GRANT_ACTIONS,
|
|
28
|
+
};
|
|
29
|
+
/** The action vocabulary for `typeKey` — the per-type override if one exists, else the default. */
|
|
30
|
+
function actionsFor(typeKey) {
|
|
31
|
+
return ACTIONS_BY_TYPE[typeKey] ?? exports.IDENTITY_ACTIONS;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-registry.bridge.d.ts","sourceRoot":"","sources":["../../src/identity/module-registry.bridge.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAc,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAQlE,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,GAAG,cAAc,CA4B9F"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildIdentityModuleRegistry = buildIdentityModuleRegistry;
|
|
4
|
+
/**
|
|
5
|
+
* Bridge: engine ObjectTypes → a populated core ModuleRegistry. Ported from astram3 (framework
|
|
6
|
+
* now owns identity). Engine ObjectTypes describe physical tables; core's gates + the
|
|
7
|
+
* permission-facts projection need object types + ACTIONS + tiers + planes. ObjectTypes carry no
|
|
8
|
+
* action list, so we synthesize per-type actions (actionsFor). Every action is `gated` — nothing
|
|
9
|
+
* open unless reclassified. `register()` throws on duplicate typeKey/permissionKey.
|
|
10
|
+
*/
|
|
11
|
+
const core_1 = require("@astram/core");
|
|
12
|
+
const engine_1 = require("@astram/engine");
|
|
13
|
+
const identity_actions_js_1 = require("./identity-actions.js");
|
|
14
|
+
function toObjectType(input) {
|
|
15
|
+
return typeof input === 'function' ? (0, engine_1.reflectObjectType)(input) : input;
|
|
16
|
+
}
|
|
17
|
+
function buildIdentityModuleRegistry(inputs) {
|
|
18
|
+
const regs = inputs.map(toObjectType);
|
|
19
|
+
const objectTypes = regs.map((r) => {
|
|
20
|
+
const a = r.access ?? {};
|
|
21
|
+
const access = {
|
|
22
|
+
...(a.owned !== undefined ? { owned: a.owned } : {}),
|
|
23
|
+
...(a.tenantScoped !== undefined ? { tenantScoped: a.tenantScoped } : {}),
|
|
24
|
+
...(a.system !== undefined ? { system: a.system } : {}),
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
typeKey: r.typeKey,
|
|
28
|
+
label: r.typeKey,
|
|
29
|
+
fields: r.fields.map((f) => ({ key: f.key, label: f.key })),
|
|
30
|
+
...(Object.keys(access).length > 0 ? { access } : {}),
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
const actions = regs.flatMap((r) => (0, identity_actions_js_1.actionsFor)(r.typeKey).map((actionKey) => ({
|
|
34
|
+
actionKey,
|
|
35
|
+
typeKey: r.typeKey,
|
|
36
|
+
tier: 'gated',
|
|
37
|
+
plane: r.access?.plane ?? 'data',
|
|
38
|
+
})));
|
|
39
|
+
const metadata = { moduleKey: identity_actions_js_1.MODULE_KEY, objectTypes, actions };
|
|
40
|
+
const registry = (0, core_1.createModuleRegistry)();
|
|
41
|
+
registry.register(metadata);
|
|
42
|
+
return registry;
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MariaDbPermissionFactsAdapter — the framework-DEFAULT source of a caller's coarse permission
|
|
3
|
+
* facts, backing `GET /identity/auth-facts` + `/identity/permissions-version`. Reads role ids from
|
|
4
|
+
* the caller's active in-effect postings (`shield_app`), the granted permission keys for those
|
|
5
|
+
* roles (`shield_authz.role_permission`), and the per-tenant permission version
|
|
6
|
+
* (`shield_authz.authz_permission_version`). Projects to `{ action, allowed:true }` facts (an
|
|
7
|
+
* absent fact is the fail-closed deny). A store outage THROWS AuthorizationUnavailableError —
|
|
8
|
+
* never a fabricated empty projection (that would silently read as "you have no grants").
|
|
9
|
+
*/
|
|
10
|
+
import type { Pool } from 'mariadb';
|
|
11
|
+
export interface AuthFactDto {
|
|
12
|
+
readonly action: string;
|
|
13
|
+
readonly allowed: boolean;
|
|
14
|
+
readonly resource?: string;
|
|
15
|
+
readonly reasonCode?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AuthFactProjection {
|
|
18
|
+
readonly principalId: string;
|
|
19
|
+
readonly facts: readonly AuthFactDto[];
|
|
20
|
+
readonly projectedAt: number;
|
|
21
|
+
readonly permissionsVersion?: number;
|
|
22
|
+
}
|
|
23
|
+
/** A store/dependency outage while computing facts — the controller maps this to 503. */
|
|
24
|
+
export declare class AuthorizationUnavailableError extends Error {
|
|
25
|
+
constructor(message?: string);
|
|
26
|
+
}
|
|
27
|
+
export declare class MariaDbPermissionFactsAdapter {
|
|
28
|
+
private readonly pool;
|
|
29
|
+
private readonly listActions;
|
|
30
|
+
private readonly controlPlaneKeys;
|
|
31
|
+
private readonly now;
|
|
32
|
+
constructor(pool: Pool, listActions: () => readonly string[], controlPlaneKeys: readonly string[], now?: () => number);
|
|
33
|
+
projectFor(principalId: string, tenantId: string, isRoot: boolean): Promise<AuthFactProjection>;
|
|
34
|
+
currentVersionFor(_principalId: string, tenantId: string): Promise<number>;
|
|
35
|
+
private readRoleIdsForTenant;
|
|
36
|
+
private readGrantKeys;
|
|
37
|
+
private readVersion;
|
|
38
|
+
/**
|
|
39
|
+
* Expand wildcard grant keys against the registry's declared actions:
|
|
40
|
+
* 'identity' / 'identity:*' → every 'identity:' action
|
|
41
|
+
* 'identity:company:*' → every 'identity:company:<action>'
|
|
42
|
+
* Exact keys pass through. Bounded to listActions() so a wildcard can never grant an
|
|
43
|
+
* action that does not exist.
|
|
44
|
+
*/
|
|
45
|
+
private expandGrantKeys;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=permission-facts.adapter.d.ts.map
|